Merge pull request #1363 from amit221/fix/issue-1255
fix(issues): normalize HTML entities in @mention tokens before agent lookup
This commit is contained in:
commit
cbca599625
2 changed files with 80 additions and 2 deletions
41
server/src/__tests__/normalize-agent-mention-token.test.ts
Normal file
41
server/src/__tests__/normalize-agent-mention-token.test.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { normalizeAgentMentionToken } from "../services/issues.ts";
|
||||||
|
|
||||||
|
describe("normalizeAgentMentionToken", () => {
|
||||||
|
it("decodes hex numeric entities such as space ( )", () => {
|
||||||
|
expect(normalizeAgentMentionToken("Baba ")).toBe("Baba");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("decodes decimal numeric entities", () => {
|
||||||
|
expect(normalizeAgentMentionToken("Baba ")).toBe("Baba");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("decodes common named whitespace entities", () => {
|
||||||
|
expect(normalizeAgentMentionToken("Baba ")).toBe("Baba");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mid-token entity (review asked for this shape); we decode &→&, not strip to "Baba" (that broke M&M).
|
||||||
|
it("decodes a named entity in the middle of the token", () => {
|
||||||
|
expect(normalizeAgentMentionToken("Ba&ba")).toBe("Ba&ba");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("decodes & so agent names with ampersands still match", () => {
|
||||||
|
expect(normalizeAgentMentionToken("M&M")).toBe("M&M");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("decodes additional named entities used in rich text (e.g. ©)", () => {
|
||||||
|
expect(normalizeAgentMentionToken("Agent©Name")).toBe("Agent©Name");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leaves unknown semicolon-terminated named references unchanged", () => {
|
||||||
|
expect(normalizeAgentMentionToken("Baba¬arealentity;")).toBe("Baba¬arealentity;");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns plain names unchanged", () => {
|
||||||
|
expect(normalizeAgentMentionToken("Baba")).toBe("Baba");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("trims after decoding entities", () => {
|
||||||
|
expect(normalizeAgentMentionToken("Baba  ")).toBe("Baba");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -233,6 +233,41 @@ function unreadForUserCondition(companyId: string, userId: string) {
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Named entities commonly emitted in saved issue bodies; unknown `&name;` sequences are left unchanged. */
|
||||||
|
const WELL_KNOWN_NAMED_HTML_ENTITIES: Readonly<Record<string, string>> = {
|
||||||
|
amp: "&",
|
||||||
|
apos: "'",
|
||||||
|
copy: "\u00A9",
|
||||||
|
gt: ">",
|
||||||
|
lt: "<",
|
||||||
|
nbsp: "\u00A0",
|
||||||
|
quot: '"',
|
||||||
|
ensp: "\u2002",
|
||||||
|
emsp: "\u2003",
|
||||||
|
thinsp: "\u2009",
|
||||||
|
};
|
||||||
|
|
||||||
|
function decodeNumericHtmlEntity(digits: string, radix: 16 | 10): string | null {
|
||||||
|
const n = Number.parseInt(digits, radix);
|
||||||
|
if (Number.isNaN(n) || n < 0 || n > 0x10ffff) return null;
|
||||||
|
try {
|
||||||
|
return String.fromCodePoint(n);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Decodes HTML character references in a raw @mention capture so UI-encoded bodies match agent names. */
|
||||||
|
export function normalizeAgentMentionToken(raw: string): string {
|
||||||
|
let s = raw.replace(/&#x([0-9a-fA-F]+);/gi, (full, hex: string) => decodeNumericHtmlEntity(hex, 16) ?? full);
|
||||||
|
s = s.replace(/&#([0-9]+);/g, (full, dec: string) => decodeNumericHtmlEntity(dec, 10) ?? full);
|
||||||
|
s = s.replace(/&([a-z][a-z0-9]*);/gi, (full, name: string) => {
|
||||||
|
const decoded = WELL_KNOWN_NAMED_HTML_ENTITIES[name.toLowerCase()];
|
||||||
|
return decoded !== undefined ? decoded : full;
|
||||||
|
});
|
||||||
|
return s.trim();
|
||||||
|
}
|
||||||
|
|
||||||
export function deriveIssueUserContext(
|
export function deriveIssueUserContext(
|
||||||
issue: IssueUserContextInput,
|
issue: IssueUserContextInput,
|
||||||
userId: string,
|
userId: string,
|
||||||
|
|
@ -1517,11 +1552,13 @@ export function issueService(db: Db) {
|
||||||
const re = /\B@([^\s@,!?.]+)/g;
|
const re = /\B@([^\s@,!?.]+)/g;
|
||||||
const tokens = new Set<string>();
|
const tokens = new Set<string>();
|
||||||
let m: RegExpExecArray | null;
|
let m: RegExpExecArray | null;
|
||||||
while ((m = re.exec(body)) !== null) tokens.add(m[1].toLowerCase());
|
while ((m = re.exec(body)) !== null) {
|
||||||
|
const normalized = normalizeAgentMentionToken(m[1]);
|
||||||
|
if (normalized) tokens.add(normalized.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
const explicitAgentMentionIds = extractAgentMentionIds(body);
|
const explicitAgentMentionIds = extractAgentMentionIds(body);
|
||||||
if (tokens.size === 0 && explicitAgentMentionIds.length === 0) return [];
|
if (tokens.size === 0 && explicitAgentMentionIds.length === 0) return [];
|
||||||
|
|
||||||
const rows = await db.select({ id: agents.id, name: agents.name })
|
const rows = await db.select({ id: agents.id, name: agents.name })
|
||||||
.from(agents).where(eq(agents.companyId, companyId));
|
.from(agents).where(eq(agents.companyId, companyId));
|
||||||
const resolved = new Set<string>(explicitAgentMentionIds);
|
const resolved = new Set<string>(explicitAgentMentionIds);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue