fix(companion): keep a named diagram topic independent of the open note

A request that names its own subject no longer reads the open note. Requests that convert or diagram the open note still do.
This commit is contained in:
tianma-if committed 2026-10-24 21:19:44 +08:00
1 parent cc8a71c399
commit 667c536958
8 files changed
+236 -23

No files matched your search

+12 -15
View File
@@ -1,4 +1,4 @@
import { COMPANION_NOTE_EDIT_MAX_CHARS, parseDiagramDocument, parseInfographicDocument, parseTableDocument, type CompanionSource, type CompanionTodo, type CompanionToolCall, type CompanionToolDefinition, type CompanionTurnInput, type MemoDetail, type MemoSummary } from "@edgeever/shared";
import { COMPANION_NOTE_EDIT_MAX_CHARS, explicitDiagramKind, independentDiagramRequest, parseDiagramDocument, parseInfographicDocument, parseTableDocument, type CompanionSource, type CompanionTodo, type CompanionToolCall, type CompanionToolDefinition, type CompanionTurnInput, type MemoDetail, type MemoSummary } from "@edgeever/shared";
import type { DatabaseAdapter } from "./storage-contract";
import type { AppContext } from "./api-context";
import type { CompanionScope } from "./companion-service";
@@ -56,18 +56,7 @@ const withOptionalInboxNotebook = (definition: CompanionToolDefinition): Compani
};
};
type ExplicitDiagramKind = "mind-map" | "flowchart" | "architecture";
const EXPLICIT_DIAGRAM_KIND_PATTERNS: Array<[ExplicitDiagramKind, RegExp]> = [
["mind-map", /(?:思维导图|思維導圖|心智图|心智圖|腦圖|mind[ -]?map|マインドマップ)/iu],
["flowchart", /(?:流程图|流程圖|flow[ -]?chart|フローチャート)/iu],
["architecture", /(?:架构图|架構圖|architectur(?:e|al)[ -]?diagram|アーキテクチャ図)/iu],
];
export const explicitDiagramKind = (message: string): ExplicitDiagramKind | undefined => {
const matches = EXPLICIT_DIAGRAM_KIND_PATTERNS.filter(([, pattern]) => pattern.test(message));
return matches.length === 1 ? matches[0][0] : undefined;
};
export { explicitDiagramKind, independentDiagramRequest };
export const requestsInfographic = (message: string) =>
/(?:信息图|資訊圖|インフォグラフィック|\binfographic\b)/iu.test(message);
@@ -123,12 +112,12 @@ const TOOL_HINTS: Record<string, string> = {
create_table_memo: " Create an editable structured table/多维表格 from the user's requested field plan. Infer useful field names and types when unspecified. Select fields need explicit options. This creates an empty table; only add records if the user requested example or supplied records. If the user did not name a notebook, omit notebookId so it is saved in the inbox (等待分类). Do not ask which notebook, and do not use the open notebook unless the user named it.",
get_table_records: " Read an existing structured table's fields, record IDs, and revision. Use this before adding a record so cell keys are actual field IDs. Do not infer field IDs from names.",
add_table_record: " Add one requested record to a structured table. First call get_table_records, then use its field IDs and latest revision. Do not invent factual records to fill a new table.",
create_diagram_memo: " Create an editable visual diagram note. kind=mind-map for 思维导图/mind map, flowchart for 流程图, architecture for 架构图. Never use this for 信息图/infographic; call create_infographic_memo. Omit edge ids; EdgeEver generates them. For mind maps, give a root and children with parentId; omit node type. If the user did not name a notebook, omit notebookId. The diagram is saved in the inbox (等待分类). Do not ask which notebook, and do not use the open notebook unless the user named it. Build nodes from the open note body in Focus DATA when the user refers to this note.",
create_diagram_memo: " Create an editable visual diagram note. kind=mind-map for 思维导图/mind map, flowchart for 流程图, architecture for 架构图. Never use this for 信息图/infographic; call create_infographic_memo. Omit edge ids; EdgeEver generates them. For mind maps, give a root and children with parentId; omit node type. If the user did not name a notebook, omit notebookId. The diagram is saved in the inbox (等待分类). Do not ask which notebook, and do not use the open notebook unless the user named it. Build from the open note when the user referred to it, attached it, or asked for a diagram without a separate subject. When they named a separate subject, such as 一个关于某主题的思维导图, build only that subject and do not read the open note.",
create_poster_memo: " Create a real editable poster (海报笔记), never poster-style Markdown. Plan the key takeaway first. For informative topics such as 产业布局, summaries, comparisons and guides, provide 3–6 useful heading/body sections, not just a heading and generic subtitle. Choose grid for categories, list for steps, and a style suited to the subject. Use referenced note content and do not invent figures or sources. Quotes/slogans/minimal covers can omit sections. The tool handles editable canvas layout and encoding. If no notebook was named, omit notebookId and use the inbox. Report failure instead of substituting prose.",
create_infographic_memo: " Create an AntV infographic note (信息图). A share, proportion, or 占比 uses template chart-pie-donut-plain-text and numeric data.values, not a mind map. If the user did not supply the figures, say in data.desc that they are illustrative and are not an official disclosure. If the user did not name a notebook, omit notebookId. The infographic is saved in the inbox (等待分类). Do not ask which notebook, and do not use the open notebook unless the user named it.",
get_poster: " Read the existing poster canvas and elements before editing. Do not use raw Markdown for posters.",
update_poster: " Edit poster text, colors, typography and positions after get_poster. Preserve unmentioned and locked elements. Pass the revision you read.",
get_diagram: " Read an existing editable diagram as a semantic graph. Call this before update_diagram. Do not use get_memo when you only need the diagram structure.",
get_diagram: " Read an existing editable diagram as a semantic graph. Call this before update_diagram. Do not use get_memo when you only need the diagram structure. Do not read the open note when the user named a separate diagram subject and did not refer to that note. Do read it when they ask to change it, or to diagram the open note.",
update_diagram: " Edit an existing diagram after get_diagram. Pass expectedRevision from get_diagram. Use add_node, update_node, remove_node, add_edge, update_edge, or remove_edge. Do not create a new diagram unless the user asked for a new note.",
use_note_template: " Create a new memo from a template. If the user did not name a notebook, omit notebookId. The note is saved in the inbox (等待分类). Do not ask which notebook.",
create_note_template: " Save a reusable note template from Markdown or from an existing memoId.",
@@ -335,6 +324,14 @@ export function createCompanionTools(args: { db: DatabaseAdapter; scope: Compani
}
return payload;
};
if ((definition.name === "get_memo" || definition.name === "get_diagram")
&& independentDiagramRequest(args.input)
&& args.input.focus?.memoId
&& String(parameters_.memoId) === args.input.focus.memoId) {
return done({
error: "The user did not refer to the open note and did not attach its content. Do not read it. Create the requested diagram from the user's topic alone with create_diagram_memo.",
});
}
const current = await companionWorkspaceCursor(args.db, args.scope.workspaceId);
session.cursor ??= current;
if (session.cursor !== current) return done({ error: "Notes changed during this request. Start a fresh request." });
+10 -6
View File
@@ -4,7 +4,7 @@ import type {
CompanionAnswer, CompanionMemory, CompanionModelContentPart, CompanionPreparedMessage, CompanionPreparedTurn,
CompanionQuestion, CompanionTodo, CompanionToolCall, CompanionTurnInput,
} from "@edgeever/shared";
import { COMPANION_MATH_FORMAT_INSTRUCTION, POSTER_TEMPLATE_SELECTION_INSTRUCTION, conversationLanguage, translationTargetInstruction } from "@edgeever/shared";
import { COMPANION_MATH_FORMAT_INSTRUCTION, INDEPENDENT_DIAGRAM_FOCUS_NOTE, OPEN_NOTE_DIAGRAM_FOCUS_NOTE, POSTER_TEMPLATE_SELECTION_INSTRUCTION, conversationLanguage, explicitDiagramKind, independentDiagramRequest, translationTargetInstruction } from "@edgeever/shared";
import { AppError } from "./app-error";
import type { DatabaseAdapter } from "./storage-contract";
import { loadCompanionAttachmentParts } from "./companion-attachments";
@@ -41,13 +41,13 @@ When listing or recommending notes, reply with a markdown link that copies the e
If grouping by notebook, put the notebook name on its own line in bold, not as a heading, and do not add emoji or icons. Keep the list compact.
Do not paste note bodies, headings, excerpts, or raw IDs. Do not use [note:ID] in user-visible replies.
Never show notebook or memo IDs as plain text; they belong only inside #memo= links.
The currently open note is only editor context. If the user names a notebook, tag, or topic, look it up with tools. Do not assume they mean the open notebook unless they say this note, this notebook, 这篇, 当前, or 这个笔记本.
The currently open note is only editor context. If the user names a notebook, tag, or topic, look it up with tools. Do not assume they mean the open notebook unless they say this notebook, 这个笔记本, or 当前笔记本. Use the open note when they refer to it (这篇, 当前, 这张图, this note), when they ask to convert or summarize it, or when they ask for a diagram without naming a separate subject. A new diagram that names its own subject, such as 生成一个关于某主题的思维导图, does not refer to the open note. Do not read or copy the open note for that request.
When asked what is in a named notebook, immediately call find_notebooks with that name, then list_memos for the match. Do not ask permission to search.
When asked what is new, recent, added, created, or updated (最近/新增/本周/this week), immediately call search_memos with createdAfter for created/added/新增 or updatedAfter for edited/updated/改过. Compute the bound from Current date. Omit query unless there is also a topic keyword; never put the time phrase in query. Do not ask which notebook or tag first. If createdAfter returns nothing for an added/新增 question, try updatedAfter before concluding there are no new notes.
search_memos matches note titles and bodies, not notebook names. Query is optional. Named tags go through list_tags or search_memos tags.
If list_memos or search_memos sets hasMore, say the list is incomplete instead of implying that is everything.
When the user refers to this note, 这篇, 当前, or the open note, use the Open note body in Focus DATA. If that body is truncated, call get_memo. If Focus DATA says the open note is a diagram, call get_diagram. Do not invent structure from the title alone.
When the user asks for a mind map, 思维导图, flowchart, 流程图, architecture diagram, or 架构图 of the open note, build it from that body (or get_memo/get_diagram first). To create a new diagram note, call create_diagram_memo. Do not use create_memo, Markdown outlines, or Mermaid. Preserve an explicitly requested type exactly: 思维导图/mind map means kind=mind-map, 流程图/flowchart means kind=flowchart, and 架构图/architecture diagram means kind=architecture. For mind maps, supply a root node and children with parentId; omit node type. If the user did not name a notebook, omit notebookId so it is saved in the inbox notebook (等待分类). Do not ask which notebook, and do not use the open notebook unless the user named it. Reply with the new note link.
When the user asks for a mind map, 思维导图, flowchart, 流程图, architecture diagram, or 架构图, call create_diagram_memo. If they named a separate subject and did not refer to the open note, build it only from the requested topic and do not call get_memo or get_diagram on the open note. If they did not name a separate subject, or they referred to the open note, build the diagram from that note: call get_memo, or get_diagram when it is a diagram. Do not use create_memo, Markdown outlines, or Mermaid. Preserve an explicitly requested type exactly: 思维导图/mind map means kind=mind-map, 流程图/flowchart means kind=flowchart, and 架构图/architecture diagram means kind=architecture. For mind maps, supply a root node and children with parentId; omit node type. If the user did not name a notebook, omit notebookId so it is saved in the inbox notebook (等待分类). Do not ask which notebook, and do not use the open notebook unless the user named it. Reply with the new note link.
${POSTER_TEMPLATE_SELECTION_INSTRUCTION}
When the user asks to generate a poster or 海报笔记, call create_poster_memo after planning the message and visual hierarchy. For informative subjects (industry layouts, explainers, comparisons, guides), supply 3–6 distinct heading/body sections and choose grid for categories or list for steps. A heading plus a generic subtitle is not a complete informative poster. Use supplied content when referenced, never invent statistics or sources, and choose a template suited to the topic. Quotes, slogans and explicitly minimal covers can omit sections. It creates the actual editable poster type. Never substitute poster-style Markdown, manually encode source, or search local files for the format. Report tool failure honestly. If the user did not name a notebook, omit notebookId. Reply with the new note link.
When the user asks for an infographic, 信息图, 資訊圖, or インフォグラフィック, call create_infographic_memo. Do not use create_diagram_memo, create_memo, Markdown, or Mermaid for that request. A share, proportion, or 占比 is a pie infographic: template chart-pie-donut-plain-text and numeric data.values, not a mind map. If the user did not supply the figures, say in data.desc that they are illustrative and are not an official disclosure. If the user did not name a notebook, omit notebookId. Reply with the new note link.
@@ -68,9 +68,11 @@ export type CompanionRunState = {
};
export function companionUserContent(input: CompanionTurnInput): string {
const focus = input.focus;
const mentions = input.mentions ?? [];
if (!focus?.memoId && !focus?.selectionMarkdown?.trim() && !focus?.contentMarkdown?.trim() && !focus?.diagramKind && !mentions.length) {
const independent = independentDiagramRequest(input);
const focus = independent ? undefined : input.focus;
const mentions = (input.mentions ?? []).filter(mention =>
!(independent && mention.type === "memo" && mention.id === input.focus?.memoId));
if (!independent && !focus?.memoId && !focus?.selectionMarkdown?.trim() && !focus?.contentMarkdown?.trim() && !focus?.diagramKind && !mentions.length) {
return input.message;
}
const lines = [
@@ -78,6 +80,8 @@ export function companionUserContent(input: CompanionTurnInput): string {
"If the user names another notebook, tag, or topic, look it up with tools instead of using this notebook.",
"If the user creates a note, diagram, or infographic without naming a notebook, omit notebookId. It is saved in the inbox notebook (等待分类). Do not ask which notebook, and do not use this open notebook unless the user named it.",
];
if (independent) lines.push(INDEPENDENT_DIAGRAM_FOCUS_NOTE);
else if (explicitDiagramKind(input.message) && input.focus?.memoId) lines.push(OPEN_NOTE_DIAGRAM_FOCUS_NOTE);
if (mentions.length) {
lines.push("Pinned context (the user attached these; treat them as the primary scope):");
for (const mention of mentions) {
@@ -397,6 +397,78 @@ describe("shared companion MCP adapter", () => {
expect(diagram.edges).toMatchObject([{ source: "web", target: "api", kind: "request" }]);
expect(diagram.edges[0].id).toBeTruthy();
});
test("a new mind map request cannot read the open diagram", async () => {
const f = await setup();
const creator = createCompanionTools({ ...f, scope, signal: new AbortController().signal, assertActive: async () => {}, sources: [] });
const created = await creator.create_diagram_memo.execute({
notebookId: "ideas",
title: "大模型蒸馏流程图",
kind: "flowchart",
nodes: [
{ id: "teacher", label: "准备教师模型", type: "process" },
{ id: "student", label: "训练学生模型", type: "process" },
],
edges: [{ source: "teacher", target: "student" }],
});
const request = {
...f.input,
message: "帮我生成一个AI大模型的思维导图。",
focus: { memoId: created.id, title: "大模型蒸馏流程图", diagramKind: "flowchart" },
};
const tools = createCompanionTools({ ...f, input: request, scope, signal: new AbortController().signal, assertActive: async () => {}, sources: [] });
const blockedDiagram = await tools.get_diagram.execute({ memoId: created.id });
expect(blockedDiagram).toMatchObject({ error: expect.stringContaining("did not refer to the open note") });
expect(blockedDiagram.diagram).toBeUndefined();
const blockedMemo = await tools.get_memo.execute({ memoId: created.id });
expect(blockedMemo).toMatchObject({ error: expect.stringContaining("did not refer to the open note") });
expect(blockedMemo.diagram).toBeUndefined();
const referred = createCompanionTools({
...f,
input: { ...request, message: "根据这篇生成思维导图" },
scope,
signal: new AbortController().signal,
assertActive: async () => {},
sources: [],
});
expect(await referred.get_diagram.execute({ memoId: created.id })).toMatchObject({
memo: { id: created.id }, diagram: { kind: "flowchart" },
});
const edit = createCompanionTools({
...f,
input: { ...request, message: "加一个评估节点" },
scope,
signal: new AbortController().signal,
assertActive: async () => {},
sources: [],
});
expect(await edit.get_diagram.execute({ memoId: created.id })).toMatchObject({ diagram: { kind: "flowchart" } });
const attached = createCompanionTools({
...f,
input: { ...request, focus: { ...request.focus, contentMarkdown: "教师模型、软标签、学生模型。" } },
scope,
signal: new AbortController().signal,
assertActive: async () => {},
sources: [],
});
expect(await attached.get_diagram.execute({ memoId: created.id })).toMatchObject({ diagram: { kind: "flowchart" } });
for (const message of ["生成思维导图", "整理成思维导图", "把这个做成思维导图"]) {
const fromOpenNote = createCompanionTools({
...f,
input: { ...request, message },
scope,
signal: new AbortController().signal,
assertActive: async () => {},
sources: [],
});
expect(await fromOpenNote.get_diagram.execute({ memoId: created.id }), message).toMatchObject({
diagram: { kind: "flowchart" },
});
}
});
test("an explicit diagram type narrows the Agent schema and rejects a mismatched write", async () => {
expect(explicitDiagramKind("帮我生成一个架构图笔记作为说明。")).toBe("architecture");
expect(explicitDiagramKind("Create a flow chart note.")).toBe("flowchart");
+36
View File
@@ -95,6 +95,40 @@ describe("companion turn context", () => {
message: "给这张图加一个评估节点",
focus: { memoId: "memo_1", notebookId: "nb_demo_features", title: "RAG 原理", diagramKind: "mind-map" },
}))).toContain("editable mind-map");
const independentDiagram = companionUserContent(input({
message: "帮我生成一个AI大模型的思维导图。",
focus: { memoId: "memo_distill", title: "大模型蒸馏流程图", diagramKind: "flowchart" },
}));
expect(independentDiagram).toContain("did not refer to the note open in the editor");
expect(independentDiagram).toContain("帮我生成一个AI大模型的思维导图。");
expect(independentDiagram).not.toContain("memo_distill");
expect(independentDiagram).not.toContain("大模型蒸馏流程图");
expect(independentDiagram).not.toContain("Call get_diagram");
const referredDiagram = companionUserContent(input({
message: "根据这篇生成思维导图",
focus: { memoId: "memo_distill", title: "大模型蒸馏流程图", diagramKind: "flowchart" },
}));
expect(referredDiagram).toContain("[note:memo_distill]");
expect(referredDiagram).toContain("Call get_diagram");
expect(referredDiagram).not.toContain("did not refer to the note open in the editor");
const attachedDiagram = companionUserContent(input({
message: "帮我生成一个AI大模型的思维导图。",
focus: {
memoId: "memo_distill", title: "大模型蒸馏流程图",
contentMarkdown: "教师模型、软标签、学生模型。",
},
}));
expect(attachedDiagram).toContain("教师模型、软标签、学生模型。");
expect(attachedDiagram).toContain("[note:memo_distill]");
for (const message of ["生成思维导图", "生成一个思维导图", "整理成思维导图", "把这个做成思维导图", "画一个流程图"]) {
const fromOpenNote = companionUserContent(input({
message,
focus: { memoId: "memo_distill", title: "大模型蒸馏流程图", diagramKind: "flowchart" },
}));
expect(fromOpenNote, message).toContain("[note:memo_distill]");
expect(fromOpenNote, message).toContain("大模型蒸馏流程图");
expect(fromOpenNote, message).not.toContain("did not refer to the note open in the editor");
}
expect(companionUserContent(input({
message: "整理这些",
mentions: [{ type: "notebook", id: "nb_study", title: "学习资料" }],
@@ -483,6 +517,8 @@ describe("actual AI SDK companion runtime", () => {
expect(COMPANION_INSTRUCTIONS).toContain("chart-pie-donut-plain-text");
expect(COMPANION_INSTRUCTIONS).toContain("思维导图");
expect(COMPANION_INSTRUCTIONS).toContain("架构图/architecture diagram means kind=architecture");
expect(COMPANION_INSTRUCTIONS).toContain("does not refer to the open note");
expect(COMPANION_INSTRUCTIONS).toContain("build it only from the requested topic");
expect(COMPANION_INSTRUCTIONS).toContain("update_diagram");
expect(COMPANION_INSTRUCTIONS).toContain("这篇");
expect(COMPANION_INSTRUCTIONS).not.toContain("You cannot create or edit diagrams");
@@ -43,6 +43,19 @@ describe("AI sidebar note context", () => {
expect(sidebarLocalContextText(selected, false, conversation())).not.toContain(note.contentMarkdown);
});
test("a new diagram request leaves the open note out of local agent context", () => {
const message = "帮我生成一个AI大模型的思维导图。";
const localContext = sidebarLocalContextText(note, false, conversation(message));
expect(localContext).toContain("did not refer to the note open in the editor");
expect(localContext).not.toContain("note-1");
expect(localContext).not.toContain(note.noteTitle);
expect(localContext).not.toContain(note.contentMarkdown);
expect(sidebarLocalContextText(note, false, conversation("根据这篇生成思维导图"))).toContain("ID: note-1");
expect(sidebarLocalContextText(note, false, conversation("生成思维导图"))).toContain("ID: note-1");
expect(sidebarLocalContextText(note, false, conversation("整理成思维导图"))).toContain(note.noteTitle);
expect(sidebarLocalContextText(note, true, conversation(message))).toContain(note.contentMarkdown);
});
test("switching notes sends only the newly open note identity", () => {
const switched = { ...note, memoId: "note-2", noteTitle: "第二篇", contentMarkdown: "第二篇正文" };
expect(sidebarCompanionFocus(switched, false)).toEqual({ memoId: "note-2", title: "第二篇" });
+13 -2
View File
@@ -1,4 +1,4 @@
import { COMPANION_MATH_FORMAT_INSTRUCTION, conversationLanguage, translationTargetInstruction, type CompanionTurnInput } from "@edgeever/shared";
import { COMPANION_MATH_FORMAT_INSTRUCTION, INDEPENDENT_DIAGRAM_FOCUS_NOTE, conversationLanguage, independentDiagramRequest, translationTargetInstruction, type CompanionTurnInput } from "@edgeever/shared";
export type SidebarNoteContext = {
memoId?: string;
@@ -37,10 +37,21 @@ export const sidebarLocalContextText = (
): string => {
const selection = context.selectionMarkdown?.trim().slice(0, 2000);
const content = includeCurrentNote ? context.contentMarkdown?.trim().slice(0, 2000) : "";
const independent = independentDiagramRequest({
message: conversation.message,
focus: context.memoId ? {
memoId: context.memoId,
...(content ? { contentMarkdown: content } : {}),
...(selection ? { selectionMarkdown: selection } : {}),
} : undefined,
});
const openNote = !context.memoId || independent
? ""
: `The following note is currently displayed in the EdgeEver UI (background data only). Its presence is not a request to open, read, summarize, or reproduce it. Follow the current user request; read note content only when needed to fulfill that request. For unrelated questions, answer directly without accessing or discussing the note. When the user refers to "this note" or the current note and needs its content, use the ID below with the connected EdgeEver MCP get_memo tool to read the full content if available:\nID: ${context.memoId}\nTitle (data): ${context.noteTitle?.trim() || "(untitled)"}`;
return [
COMPANION_MATH_FORMAT_INSTRUCTION,
translationTargetInstruction(conversationLanguage(conversation.message, conversation.recentUserMessages, conversation.fallbackLocale)),
context.memoId ? `The following note is currently displayed in the EdgeEver UI (background data only). Its presence is not a request to open, read, summarize, or reproduce it. Follow the current user request; read note content only when needed to fulfill that request. For unrelated questions, answer directly without accessing or discussing the note. When the user refers to "this note" or the current note and needs its content, use the ID below with the connected EdgeEver MCP get_memo tool to read the full content if available:\nID: ${context.memoId}\nTitle (data): ${context.noteTitle?.trim() || "(untitled)"}` : "",
independent ? INDEPENDENT_DIAGRAM_FOCUS_NOTE : openNote,
selection ? `Selected text from the current note (data):\n${selection}` : "",
content ? `Current note body (data):\n${content}` : "",
].filter(Boolean).join("\n\n");
+79
View File
@@ -0,0 +1,79 @@
import type { CompanionTurnInput } from "./companion";
type ExplicitDiagramKind = "mind-map" | "flowchart" | "architecture";
const EXPLICIT_DIAGRAM_KIND_PATTERNS: Array<[ExplicitDiagramKind, RegExp]> = [
["mind-map", /(?:思维导图|思維導圖|心智图|心智圖|腦圖|mind[ -]?map|マインドマップ)/iu],
["flowchart", /(?:流程图|流程圖|flow[ -]?chart|フローチャート)/iu],
["architecture", /(?:架构图|架構圖|architectur(?:e|al)[ -]?diagram|アーキテクチャ図)/iu],
];
// Deictic references to the note open in the editor. A shared topic word is not one of these.
const OPEN_NOTE_REFERENCE = /(?:这篇|此篇|本文|本篇|这条笔记|这个笔记|这份笔记|上述|当前|上面|这[张个幅]图|这张流程|这张导图|打开的笔记|正在看的|笔记里|图里|根据这篇|基于这篇|照着这|this note|this diagram|this flowchart|this mind[- ]?map|the open note|current note|current diagram|このノート|この図|このフロー|このマインドマップ|現在のノート|このメモ|ta notatk[aę]|tej notatki|bieżącej notatki|bieżąca notatka)/iu;
const DIAGRAM_KIND_SOURCE = EXPLICIT_DIAGRAM_KIND_PATTERNS.map(([, pattern]) => pattern.source).join("|");
const OWN_SUBJECT_BEFORE = new RegExp(
`(?:生成|新建|创建|绘制|画|做|写|制作|create|draw|make|generate)(?:一[个张份])?(?:关于|有关|围绕)?([^,。,.!!??\\n]{0,40}?)(?:的)?(?:${DIAGRAM_KIND_SOURCE})`,
"iu",
);
const OWN_SUBJECT_AFTER = new RegExp(
`(?:${DIAGRAM_KIND_SOURCE})(?:\\s*[,,]?\\s*)?(?:关于|有关|主题是|主题为|来介绍|用来介绍|介绍一下|about|of|on|for)\\s*([^,。,.!!??\\n]{1,40})`,
"iu",
);
// 整理成 / 画成 / 做成 keep the open note. They are not a separately named subject.
const OPERATE_ON_OPEN_NOTE = new RegExp(
`(?:整理|转换|转|画|做|总结|归纳|提炼|换成|变成)成\\s*(?:一[个张份])?\\s*(?:${DIAGRAM_KIND_SOURCE})`,
"iu",
);
const LEADING_FILLER = /^(?:帮我|请|给我|一下|一个|一张|一份|关于|有关|围绕|的|把|将|a|an|the)\s*/iu;
const TRAILING_FILLER = /(?:的|一下|笔记|note)\s*$/iu;
const DEICTIC_TOPIC = /^(?:这个|这篇|此篇|本文|本篇|当前|上面|它|这张|这张图|这个图|内容|这些|该图|该笔记|笔记内容|this|it|that)$/iu;
export const INDEPENDENT_DIAGRAM_FOCUS_NOTE = "The user named a separate diagram subject and did not refer to the note open in the editor. Do not call get_memo or get_diagram on it. Do not reuse its title or structure. Build the diagram only from this request.";
export const OPEN_NOTE_DIAGRAM_FOCUS_NOTE = "The user asked for a diagram of the open note, or did not name a separate subject. Build it from that note. If its body is not included below, call get_memo, or get_diagram when the open note is a diagram.";
export type OpenNoteFocusInput = {
message: string;
focus?: Pick<NonNullable<CompanionTurnInput["focus"]>, "memoId" | "contentMarkdown" | "selectionMarkdown"> | null;
mentions?: ReadonlyArray<{ type: string; id: string }>;
};
export const explicitDiagramKind = (message: string): ExplicitDiagramKind | undefined => {
const matches = EXPLICIT_DIAGRAM_KIND_PATTERNS.filter(([, pattern]) => pattern.test(message));
return matches.length === 1 ? matches[0][0] : undefined;
};
export const messageRefersToOpenNote = (message: string) => OPEN_NOTE_REFERENCE.test(message);
const substantiveTopic = (value: string | undefined) => {
let topic = (value ?? "").trim();
for (let i = 0; i < 6 && topic; i += 1) {
const next = topic.replace(LEADING_FILLER, "").replace(TRAILING_FILLER, "").trim();
if (next === topic) break;
topic = next;
}
if (!topic || DEICTIC_TOPIC.test(topic)) return "";
return topic;
};
// True only when the user names a subject of their own, such as 「一个 AI 大模型的思维导图」.
// 「生成思维导图」 or 「整理成思维导图」 has no separate subject, so the open note stays in scope.
export const diagramRequestHasOwnSubject = (message: string) => {
if (!explicitDiagramKind(message) || messageRefersToOpenNote(message) || OPERATE_ON_OPEN_NOTE.test(message)) return false;
const before = substantiveTopic(message.match(OWN_SUBJECT_BEFORE)?.[1]);
const after = substantiveTopic(message.match(OWN_SUBJECT_AFTER)?.[1]);
return Boolean(before || after);
};
export const openNoteContentAttached = (input: OpenNoteFocusInput) => {
const focus = input.focus;
if (!focus?.memoId) return false;
if (focus.contentMarkdown?.trim() || focus.selectionMarkdown?.trim()) return true;
return (input.mentions ?? []).some(mention => mention.type === "memo" && mention.id === focus.memoId);
};
export const independentDiagramRequest = (input: OpenNoteFocusInput) =>
Boolean(input.focus?.memoId)
&& diagramRequestHasOwnSubject(input.message)
&& !openNoteContentAttached(input);
+1
View File
@@ -2,6 +2,7 @@ export * from "./content";
export * from "./document-extensions";
export * from "./image-alignment";
export * from "./companion";
export * from "./companion-focus";
export * from "./public-demo";
export * from "./ai-assistant";
export * from "./ai-generation";