|
|
|
@@ -101,14 +101,12 @@ const DEFAULT_TOOLS: MnoteToolManifestEntry[] = [
|
|
|
|
|
|
|
|
|
|
const EXTENSION_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
|
|
|
const DEFAULT_CONTEXT_FILE = path.join(EXTENSION_DIR, "mnote-context.json");
|
|
|
|
|
const EMBEDDED_CONTEXT: Record<string, unknown> | undefined = undefined;
|
|
|
|
|
const CONTEXT_FILE = env("PI_MNOTE_CONTEXT_FILE")
|
|
|
|
|
|| (fs.existsSync(DEFAULT_CONTEXT_FILE) ? DEFAULT_CONTEXT_FILE : "");
|
|
|
|
|
|| DEFAULT_CONTEXT_FILE;
|
|
|
|
|
const RUNTIME_IMPL = env("PI_MNOTE_RUNTIME_IMPL")
|
|
|
|
|
|| env("MNOTE_PI_RUNTIME_IMPL")
|
|
|
|
|
|| (CONTEXT_FILE ? "pi-rust" : "");
|
|
|
|
|
const BASE_URL = env("PI_MNOTE_BRIDGE_BASE_URL") || env("MNOTE_PI_BRIDGE_BASE_URL") || env("MNOTE_PI_LAB_BASE_URL") || "http://127.0.0.1:3000";
|
|
|
|
|
const SESSION_ID = env("PI_MNOTE_BRIDGE_SESSION_ID") || env("MNOTE_PI_BRIDGE_SESSION_ID") || env("MNOTE_PI_LAB_SESSION_ID") || "";
|
|
|
|
|
const BRIDGE_TOKEN = env("PI_MNOTE_BRIDGE_TOKEN") || env("MNOTE_PI_BRIDGE_TOKEN") || env("MNOTE_PI_LAB_BRIDGE_TOKEN") || "";
|
|
|
|
|
const HTTP_BRIDGE_AVAILABLE = env("PI_MNOTE_HTTP_BRIDGE_AVAILABLE") === "1";
|
|
|
|
|
const TOOL_POLICIES = parseJsonRecord(env("PI_MNOTE_BRIDGE_TOOL_POLICIES") || env("MNOTE_PI_BRIDGE_TOOL_POLICIES"), {});
|
|
|
|
|
const TOOLS = normalizeTools(parseJsonUnknown(env("PI_MNOTE_BRIDGE_TOOLS") || env("MNOTE_PI_BRIDGE_TOOLS")) ?? DEFAULT_TOOLS);
|
|
|
|
@@ -119,6 +117,10 @@ function env(name: string): string {
|
|
|
|
|
return (process.env[name] || "").trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function contextFileExists(): boolean {
|
|
|
|
|
return Boolean(CONTEXT_FILE) && fs.existsSync(CONTEXT_FILE);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parseJsonUnknown(raw: string): unknown {
|
|
|
|
|
if (!raw) return undefined;
|
|
|
|
|
try {
|
|
|
|
@@ -157,6 +159,23 @@ function stringField(record: Record<string, unknown>, key: string): string {
|
|
|
|
|
return typeof value === "string" ? value.trim() : "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function recordField(record: Record<string, unknown> | undefined, key: string): Record<string, unknown> | undefined {
|
|
|
|
|
if (!record) return undefined;
|
|
|
|
|
const value = record[key];
|
|
|
|
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
|
|
|
? value as Record<string, unknown>
|
|
|
|
|
: undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function selectedContextPart(context: Record<string, unknown>, key: string): Record<string, unknown> | undefined {
|
|
|
|
|
return recordField(recordField(context, "selectedContext"), key);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function contextRefs(context: Record<string, unknown>): string[] {
|
|
|
|
|
const refs = context.contextRefs;
|
|
|
|
|
return Array.isArray(refs) ? refs.filter((ref): ref is string => typeof ref === "string") : [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toolResult(payload: Record<string, unknown>, isError = false) {
|
|
|
|
|
return {
|
|
|
|
|
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
|
|
@@ -201,10 +220,9 @@ function captureInputContext(event: Record<string, unknown>) {
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function readContextSnapshot(): Record<string, unknown> {
|
|
|
|
|
if (liveContext) return liveContext;
|
|
|
|
|
if (!CONTEXT_FILE) {
|
|
|
|
|
throw new Error("PI_MNOTE_CONTEXT_FILE 未配置");
|
|
|
|
|
function readContextFileSnapshot(): Record<string, unknown> {
|
|
|
|
|
if (!contextFileExists()) {
|
|
|
|
|
throw new Error("MNote Pi 上下文快照不可用;Pi Rust bridge 需要 input hook、embedded context 或 context file");
|
|
|
|
|
}
|
|
|
|
|
const raw = fs.readFileSync(CONTEXT_FILE, "utf8");
|
|
|
|
|
const parsed = JSON.parse(raw);
|
|
|
|
@@ -214,6 +232,63 @@ function readContextSnapshot(): Record<string, unknown> {
|
|
|
|
|
return parsed as Record<string, unknown>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function embeddedContextSnapshot(): Record<string, unknown> | undefined {
|
|
|
|
|
return EMBEDDED_CONTEXT && typeof EMBEDDED_CONTEXT === "object" && !Array.isArray(EMBEDDED_CONTEXT)
|
|
|
|
|
? EMBEDDED_CONTEXT
|
|
|
|
|
: undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function activeContextSnapshot(): Record<string, unknown> | undefined {
|
|
|
|
|
try {
|
|
|
|
|
if (contextFileExists()) return readContextFileSnapshot();
|
|
|
|
|
} catch {
|
|
|
|
|
// Fall back to the input-hook snapshot when the host cannot read files.
|
|
|
|
|
}
|
|
|
|
|
return liveContext || embeddedContextSnapshot();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function readContextSnapshot(): Record<string, unknown> {
|
|
|
|
|
const snapshot = activeContextSnapshot();
|
|
|
|
|
if (snapshot) return snapshot;
|
|
|
|
|
return readContextFileSnapshot();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function bridgeToken(): string {
|
|
|
|
|
const fromEnv = env("PI_MNOTE_BRIDGE_TOKEN")
|
|
|
|
|
|| env("MNOTE_PI_BRIDGE_TOKEN")
|
|
|
|
|
|| env("MNOTE_PI_LAB_BRIDGE_TOKEN");
|
|
|
|
|
if (fromEnv) return fromEnv;
|
|
|
|
|
const fromContext = activeContextSnapshot();
|
|
|
|
|
if (fromContext) {
|
|
|
|
|
const value = stringField(fromContext, "bridgeToken");
|
|
|
|
|
if (value) return value;
|
|
|
|
|
}
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function bridgeBaseUrl(): string {
|
|
|
|
|
const fromEnv = env("PI_MNOTE_BRIDGE_BASE_URL")
|
|
|
|
|
|| env("MNOTE_PI_BRIDGE_BASE_URL")
|
|
|
|
|
|| env("MNOTE_PI_LAB_BASE_URL");
|
|
|
|
|
if (fromEnv) return fromEnv;
|
|
|
|
|
const fromContext = activeContextSnapshot();
|
|
|
|
|
if (fromContext) {
|
|
|
|
|
const value = stringField(fromContext, "bridgeBaseUrl");
|
|
|
|
|
if (value) return value;
|
|
|
|
|
}
|
|
|
|
|
return "http://127.0.0.1:3000";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function bridgeSessionId(): string {
|
|
|
|
|
const fromContext = activeContextSnapshot();
|
|
|
|
|
const fromContextSession = fromContext ? stringField(fromContext, "sessionId") : "";
|
|
|
|
|
if (fromContextSession) return fromContextSession;
|
|
|
|
|
const fromEnv = env("PI_MNOTE_BRIDGE_SESSION_ID")
|
|
|
|
|
|| env("MNOTE_PI_BRIDGE_SESSION_ID");
|
|
|
|
|
if (fromEnv) return fromEnv;
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function contextAllowedRoots(context: Record<string, unknown>): Record<string, unknown>[] {
|
|
|
|
|
const snapshot = context.allowedRoots;
|
|
|
|
|
if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) return [];
|
|
|
|
@@ -233,6 +308,7 @@ function resolveAllowedTarget(
|
|
|
|
|
context: Record<string, unknown>,
|
|
|
|
|
rootUri: string,
|
|
|
|
|
relativePath: string,
|
|
|
|
|
options?: { allowMissing?: boolean },
|
|
|
|
|
): { target: string; readPath: string } {
|
|
|
|
|
const roots = contextAllowedRoots(context);
|
|
|
|
|
const matchedRoot = roots.find((root) => stringField(root, "rootUri") === rootUri);
|
|
|
|
@@ -245,7 +321,9 @@ function resolveAllowedTarget(
|
|
|
|
|
const requestedTarget = path.isAbsolute(relativePath)
|
|
|
|
|
? path.resolve(relativePath)
|
|
|
|
|
: path.resolve(canonicalRoot, relativePath);
|
|
|
|
|
const canonicalTarget = fs.realpathSync(requestedTarget);
|
|
|
|
|
const canonicalTarget = options?.allowMissing
|
|
|
|
|
? canonicalOrParent(requestedTarget)
|
|
|
|
|
: fs.realpathSync(requestedTarget);
|
|
|
|
|
const allowedRoots = roots
|
|
|
|
|
.map((root) => stringField(root, "rootPath"))
|
|
|
|
|
.filter(Boolean)
|
|
|
|
@@ -261,16 +339,96 @@ function resolveAllowedTarget(
|
|
|
|
|
return { target: canonicalTarget, readPath };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function canonicalOrParent(target: string): string {
|
|
|
|
|
try {
|
|
|
|
|
return fs.realpathSync(target);
|
|
|
|
|
} catch {
|
|
|
|
|
const parent = path.dirname(target);
|
|
|
|
|
try {
|
|
|
|
|
return path.join(fs.realpathSync(parent), path.basename(target));
|
|
|
|
|
} catch {
|
|
|
|
|
return path.resolve(target);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function sessionRootUri(context: Record<string, unknown>, input: Record<string, unknown>): string {
|
|
|
|
|
return stringField(input, "rootUri")
|
|
|
|
|
|| stringField(input, "root_uri")
|
|
|
|
|
|| stringField(selectedContextPart(context, "currentPage") || {}, "rootUri")
|
|
|
|
|
|| stringField(selectedContextPart(context, "currentFolder") || {}, "rootUri")
|
|
|
|
|
|| stringField(context, "rootUri");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function joinRelativePath(basePath: string, childPath: string): string {
|
|
|
|
|
const normalizedChild = childPath.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
|
|
|
const normalizedBase = basePath.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
|
|
|
|
|
if (!normalizedBase || path.isAbsolute(childPath)) return normalizedChild;
|
|
|
|
|
if (!normalizedChild || normalizedChild === ".") return normalizedBase;
|
|
|
|
|
if (normalizedChild === normalizedBase || normalizedChild.startsWith(`${normalizedBase}/`)) return normalizedChild;
|
|
|
|
|
return `${normalizedBase}/${normalizedChild}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function sessionRelativePath(context: Record<string, unknown>, input: Record<string, unknown>, options?: { preferFolder?: boolean }): string {
|
|
|
|
|
const explicitPath = stringField(input, "relativePath")
|
|
|
|
|
|| stringField(input, "relative_path")
|
|
|
|
|
|| stringField(input, "path");
|
|
|
|
|
const folderPath = stringField(input, "folderPath")
|
|
|
|
|
|| stringField(input, "folder_path")
|
|
|
|
|
|| stringField(selectedContextPart(context, "currentFolder") || {}, "folderPath");
|
|
|
|
|
if (explicitPath) {
|
|
|
|
|
return options?.preferFolder ? joinRelativePath(folderPath, explicitPath) : explicitPath;
|
|
|
|
|
}
|
|
|
|
|
return stringField(input, "pagePath")
|
|
|
|
|
|| stringField(input, "page_path")
|
|
|
|
|
|| stringField(selectedContextPart(context, "currentPage") || {}, "pagePath")
|
|
|
|
|
|| folderPath
|
|
|
|
|
|| stringField(context, "pagePath");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function contextualToolParams(toolName: string, params: unknown): Record<string, unknown> {
|
|
|
|
|
const nextParams = { ...((params || {}) as Record<string, unknown>) };
|
|
|
|
|
const context = activeContextSnapshot();
|
|
|
|
|
if (!context) return nextParams;
|
|
|
|
|
const currentPage = selectedContextPart(context, "currentPage");
|
|
|
|
|
const currentFolder = selectedContextPart(context, "currentFolder");
|
|
|
|
|
const selection = selectedContextPart(context, "selection");
|
|
|
|
|
if (!stringField(nextParams, "rootUri")) {
|
|
|
|
|
nextParams.rootUri = stringField(currentPage || {}, "rootUri")
|
|
|
|
|
|| stringField(currentFolder || {}, "rootUri")
|
|
|
|
|
|| stringField(context, "rootUri")
|
|
|
|
|
|| undefined;
|
|
|
|
|
}
|
|
|
|
|
if (!stringField(nextParams, "workspaceId")) {
|
|
|
|
|
nextParams.workspaceId = stringField(currentPage || {}, "workspaceId")
|
|
|
|
|
|| stringField(currentFolder || {}, "workspaceId")
|
|
|
|
|
|| stringField(context, "workspaceId")
|
|
|
|
|
|| undefined;
|
|
|
|
|
}
|
|
|
|
|
if (!stringField(nextParams, "pagePath")) {
|
|
|
|
|
nextParams.pagePath = stringField(currentPage || {}, "pagePath") || undefined;
|
|
|
|
|
}
|
|
|
|
|
if (!stringField(nextParams, "folderPath")) {
|
|
|
|
|
nextParams.folderPath = stringField(currentFolder || {}, "folderPath") || undefined;
|
|
|
|
|
}
|
|
|
|
|
if (toolName === "mnote.selection.read") {
|
|
|
|
|
nextParams.selectionSource = "mnote_sidebar_host";
|
|
|
|
|
nextParams.selection = selection || null;
|
|
|
|
|
}
|
|
|
|
|
return nextParams;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function executeNativeCurrentPageRead(params: unknown) {
|
|
|
|
|
try {
|
|
|
|
|
const input = params && typeof params === "object" && !Array.isArray(params)
|
|
|
|
|
? params as Record<string, unknown>
|
|
|
|
|
: {};
|
|
|
|
|
const context = readContextSnapshot();
|
|
|
|
|
const rootUri = stringField(context, "rootUri")
|
|
|
|
|
const currentPage = selectedContextPart(context, "currentPage");
|
|
|
|
|
const rootUri = stringField(currentPage || {}, "rootUri")
|
|
|
|
|
|| stringField(input, "rootUri")
|
|
|
|
|
|| stringField(input, "root_uri");
|
|
|
|
|
const pagePath = stringField(context, "pagePath")
|
|
|
|
|
const pagePath = stringField(currentPage || {}, "pagePath")
|
|
|
|
|
|| stringField(input, "pagePath")
|
|
|
|
|
|| stringField(input, "page_path")
|
|
|
|
|
|| stringField(input, "path");
|
|
|
|
@@ -291,6 +449,8 @@ function executeNativeCurrentPageRead(params: unknown) {
|
|
|
|
|
fileVersion: `pi-rust-native-${Math.trunc(stat.mtimeMs)}-${stat.size}`,
|
|
|
|
|
transport: "pi-rust-native-fs",
|
|
|
|
|
contextFile: CONTEXT_FILE,
|
|
|
|
|
contextRefs: contextRefs(context),
|
|
|
|
|
selectedContextCurrentPage: currentPage ?? null,
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return toolResult({
|
|
|
|
@@ -303,12 +463,118 @@ function executeNativeCurrentPageRead(params: unknown) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function executeNativeLocalFileRead(params: unknown) {
|
|
|
|
|
try {
|
|
|
|
|
const input = params && typeof params === "object" && !Array.isArray(params)
|
|
|
|
|
? params as Record<string, unknown>
|
|
|
|
|
: {};
|
|
|
|
|
const context = readContextSnapshot();
|
|
|
|
|
const rootUri = sessionRootUri(context, input);
|
|
|
|
|
const relativePath = sessionRelativePath(context, input, { preferFolder: true });
|
|
|
|
|
if (!rootUri) throw new Error("读取文件缺少 rootUri");
|
|
|
|
|
if (!relativePath) throw new Error("读取文件缺少 relativePath/path");
|
|
|
|
|
const resolved = resolveAllowedTarget(context, rootUri, relativePath);
|
|
|
|
|
const content = fs.readFileSync(resolved.readPath, "utf8");
|
|
|
|
|
const stat = fs.statSync(resolved.readPath);
|
|
|
|
|
return toolResult({
|
|
|
|
|
ok: true,
|
|
|
|
|
rootUri,
|
|
|
|
|
relativePath,
|
|
|
|
|
path: resolved.target,
|
|
|
|
|
content,
|
|
|
|
|
contentLength: content.length,
|
|
|
|
|
fileVersion: `pi-rust-native-${Math.trunc(stat.mtimeMs)}-${stat.size}`,
|
|
|
|
|
transport: "pi-rust-native-fs",
|
|
|
|
|
contextFile: CONTEXT_FILE,
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return toolResult({
|
|
|
|
|
ok: false,
|
|
|
|
|
code: "mnote_pi_rust_local_file_read_failed",
|
|
|
|
|
message: error instanceof Error ? error.message : String(error),
|
|
|
|
|
transport: "pi-rust-native-fs",
|
|
|
|
|
contextFile: CONTEXT_FILE,
|
|
|
|
|
}, true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function applyTextOperations(current: string, operations: unknown): string {
|
|
|
|
|
if (!Array.isArray(operations)) throw new Error("operations 必须是数组");
|
|
|
|
|
let next = current;
|
|
|
|
|
for (const operation of operations) {
|
|
|
|
|
if (!operation || typeof operation !== "object" || Array.isArray(operation)) {
|
|
|
|
|
throw new Error("operation 格式无效");
|
|
|
|
|
}
|
|
|
|
|
const op = operation as Record<string, unknown>;
|
|
|
|
|
const type = stringField(op, "type") || stringField(op, "op");
|
|
|
|
|
if (type === "replace") {
|
|
|
|
|
const oldText = typeof op.oldText === "string" ? op.oldText : typeof op.old_text === "string" ? op.old_text : "";
|
|
|
|
|
const newText = typeof op.newText === "string" ? op.newText : typeof op.new_text === "string" ? op.new_text : "";
|
|
|
|
|
if (!oldText) throw new Error("replace operation 缺少 oldText");
|
|
|
|
|
const index = next.indexOf(oldText);
|
|
|
|
|
if (index < 0) throw new Error("replace operation 未找到 oldText");
|
|
|
|
|
next = next.slice(0, index) + newText + next.slice(index + oldText.length);
|
|
|
|
|
} else if (type === "append") {
|
|
|
|
|
const text = typeof op.text === "string" ? op.text : "";
|
|
|
|
|
next += text;
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error(`不支持的 operation: ${type || "unknown"}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return next;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function executeNativeLocalFilePatch(params: unknown) {
|
|
|
|
|
try {
|
|
|
|
|
const input = params && typeof params === "object" && !Array.isArray(params)
|
|
|
|
|
? params as Record<string, unknown>
|
|
|
|
|
: {};
|
|
|
|
|
const context = readContextSnapshot();
|
|
|
|
|
const rootUri = sessionRootUri(context, input);
|
|
|
|
|
const relativePath = sessionRelativePath(context, input, { preferFolder: true });
|
|
|
|
|
if (!rootUri) throw new Error("写入文件缺少 rootUri");
|
|
|
|
|
if (!relativePath) throw new Error("写入文件缺少 relativePath/path");
|
|
|
|
|
const resolved = resolveAllowedTarget(context, rootUri, relativePath, { allowMissing: true });
|
|
|
|
|
const before = fs.existsSync(resolved.target) ? fs.readFileSync(resolved.target, "utf8") : "";
|
|
|
|
|
const next = typeof input.content === "string"
|
|
|
|
|
? input.content
|
|
|
|
|
: applyTextOperations(before, input.operations);
|
|
|
|
|
fs.mkdirSync(path.dirname(resolved.target), { recursive: true });
|
|
|
|
|
fs.writeFileSync(resolved.target, next, "utf8");
|
|
|
|
|
const stat = fs.statSync(resolved.target);
|
|
|
|
|
return toolResult({
|
|
|
|
|
ok: true,
|
|
|
|
|
rootUri,
|
|
|
|
|
relativePath,
|
|
|
|
|
path: resolved.target,
|
|
|
|
|
beforeFileVersion: `pi-rust-native-before-${before.length}`,
|
|
|
|
|
afterFileVersion: `pi-rust-native-${Math.trunc(stat.mtimeMs)}-${stat.size}`,
|
|
|
|
|
oldSize: before.length,
|
|
|
|
|
newSize: next.length,
|
|
|
|
|
diffSummary: before === next ? "no_changes" : `bytes_delta=${next.length - before.length}`,
|
|
|
|
|
refresh: "mnote local-folder watcher / document-session external refresh",
|
|
|
|
|
transport: "pi-rust-native-fs",
|
|
|
|
|
contextFile: CONTEXT_FILE,
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return toolResult({
|
|
|
|
|
ok: false,
|
|
|
|
|
code: "mnote_pi_rust_local_file_patch_failed",
|
|
|
|
|
message: error instanceof Error ? error.message : String(error),
|
|
|
|
|
transport: "pi-rust-native-fs",
|
|
|
|
|
contextFile: CONTEXT_FILE,
|
|
|
|
|
}, true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function executeNativeSelectionRead() {
|
|
|
|
|
try {
|
|
|
|
|
const context = readContextSnapshot();
|
|
|
|
|
const selection = selectedContextPart(context, "selection");
|
|
|
|
|
return toolResult({
|
|
|
|
|
ok: true,
|
|
|
|
|
selection: context.selectedContext ?? null,
|
|
|
|
|
selection: selection ?? null,
|
|
|
|
|
text: stringField(selection || {}, "text"),
|
|
|
|
|
selectionSource: "mnote_sidebar_host_snapshot",
|
|
|
|
|
rootUri: context.rootUri ?? null,
|
|
|
|
|
pagePath: context.pagePath ?? null,
|
|
|
|
@@ -333,6 +599,9 @@ function executeNativeAllowedRootsDescribe() {
|
|
|
|
|
allowedRoots: context.allowedRoots ?? { roots: [] },
|
|
|
|
|
rootUri: context.rootUri ?? null,
|
|
|
|
|
pagePath: context.pagePath ?? null,
|
|
|
|
|
workspaceId: context.workspaceId ?? null,
|
|
|
|
|
contextRefs: context.contextRefs ?? [],
|
|
|
|
|
selectedContext: context.selectedContext ?? null,
|
|
|
|
|
primaryRootPath: context.primaryRootPath ?? process.cwd(),
|
|
|
|
|
transport: "pi-rust-native-context-file",
|
|
|
|
|
});
|
|
|
|
@@ -346,31 +615,63 @@ function executeNativeAllowedRootsDescribe() {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isPiRustNativeRuntime(): boolean {
|
|
|
|
|
return runtimeImplementation() === "pi-rust" || Boolean(CONTEXT_FILE);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function executeNativeTool(tool: MnoteToolManifestEntry, params: unknown) {
|
|
|
|
|
if (runtimeImplementation() !== "pi-rust" || toolPolicy(tool.mnoteName) !== "allow") return undefined;
|
|
|
|
|
if (!isPiRustNativeRuntime() || toolPolicy(tool.mnoteName) !== "allow") return undefined;
|
|
|
|
|
if (tool.mnoteName === "mnote.current_page.read") return executeNativeCurrentPageRead(params);
|
|
|
|
|
if (tool.mnoteName === "mnote.selection.read") return executeNativeSelectionRead();
|
|
|
|
|
if (tool.mnoteName === "mnote.allowed_roots.describe") return executeNativeAllowedRootsDescribe();
|
|
|
|
|
if (tool.mnoteName === "mnote.local_file.read") return executeNativeLocalFileRead(params);
|
|
|
|
|
if (tool.mnoteName === "mnote.local_file.patch") return executeNativeLocalFilePatch(params);
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function callMnote(toolName: string, params: unknown) {
|
|
|
|
|
const response = await fetch(`${BASE_URL}/api/page-ai/pi/tool-call-bridge`, {
|
|
|
|
|
const sessionId = bridgeSessionId();
|
|
|
|
|
if (!sessionId) {
|
|
|
|
|
return toolResult({
|
|
|
|
|
ok: false,
|
|
|
|
|
code: "mnote_pi_bridge_session_id_missing",
|
|
|
|
|
message: "MNote Pi bridge 缺少有效的 pi_lab sessionId;已忽略 Pi runtime provider session id。",
|
|
|
|
|
toolName,
|
|
|
|
|
transport: "mnote-bridge-http",
|
|
|
|
|
}, true);
|
|
|
|
|
}
|
|
|
|
|
const response = await fetch(`${bridgeBaseUrl()}/api/page-ai/pi/tool-call-bridge`, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: {
|
|
|
|
|
"content-type": "application/json",
|
|
|
|
|
"x-mnote-pi-lab-bridge-token": BRIDGE_TOKEN,
|
|
|
|
|
"x-mnote-pi-lab-bridge-token": bridgeToken(),
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({ sessionId: SESSION_ID, toolName, params: params || {} }),
|
|
|
|
|
body: JSON.stringify({ sessionId, toolName, params: params || {} }),
|
|
|
|
|
});
|
|
|
|
|
const payload = await response.json().catch(() => ({ ok: false, code: "bad_json" }));
|
|
|
|
|
const text = JSON.stringify((payload as Record<string, unknown>).result || payload, null, 2);
|
|
|
|
|
const result = (payload as Record<string, unknown>).result || payload;
|
|
|
|
|
const text = JSON.stringify(modelSafeBridgeResult(toolName, result), null, 2);
|
|
|
|
|
return {
|
|
|
|
|
content: [{ type: "text", text }],
|
|
|
|
|
details: payload,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function modelSafeBridgeResult(toolName: string, value: unknown): unknown {
|
|
|
|
|
if (toolName !== "mnote.knowledge_rag.query" || !value || typeof value !== "object" || Array.isArray(value)) {
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
const result = { ...(value as Record<string, unknown>) };
|
|
|
|
|
delete result.uiCitations;
|
|
|
|
|
delete result.citationMarkdowns;
|
|
|
|
|
delete result.citationRendering;
|
|
|
|
|
result.uiCitationCount = Array.isArray((value as Record<string, unknown>).uiCitations)
|
|
|
|
|
? ((value as Record<string, unknown>).uiCitations as unknown[]).length
|
|
|
|
|
: 0;
|
|
|
|
|
result.uiCitationDelivery = "Clickable citations are retained in tool details for the MNote UI and omitted from model context.";
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function stableJson(value: unknown): string {
|
|
|
|
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
|
|
|
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
|
|
@@ -394,8 +695,8 @@ function paramsHash(params: unknown): string {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toolPolicy(toolName: string): string {
|
|
|
|
|
const contextPolicies = liveContext?.toolPolicies;
|
|
|
|
|
const contextValue = contextPolicies && typeof contextPolicies === "object" && !Array.isArray(contextPolicies)
|
|
|
|
|
const contextPolicies = activeContextSnapshot()?.toolPolicies;
|
|
|
|
|
let contextValue = contextPolicies && typeof contextPolicies === "object" && !Array.isArray(contextPolicies)
|
|
|
|
|
? (contextPolicies as Record<string, unknown>)[toolName]
|
|
|
|
|
: undefined;
|
|
|
|
|
const value = contextValue ?? TOOL_POLICIES[toolName];
|
|
|
|
@@ -403,17 +704,27 @@ function toolPolicy(toolName: string): string {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function runtimeImplementation(): string {
|
|
|
|
|
return liveContext ? stringField(liveContext, "runtimeImplementation") || RUNTIME_IMPL : RUNTIME_IMPL;
|
|
|
|
|
const fromContext = activeContextSnapshot();
|
|
|
|
|
return fromContext ? stringField(fromContext, "runtimeImplementation") || RUNTIME_IMPL : RUNTIME_IMPL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function requestMnoteApprovalViaBridge(request: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
|
|
|
const response = await fetch(`${BASE_URL}/api/page-ai/pi/ui-request-bridge`, {
|
|
|
|
|
const sessionId = bridgeSessionId();
|
|
|
|
|
if (!sessionId) {
|
|
|
|
|
return {
|
|
|
|
|
ok: false,
|
|
|
|
|
cancelled: true,
|
|
|
|
|
code: "mnote_pi_bridge_session_id_missing",
|
|
|
|
|
message: "MNote Pi bridge 缺少有效的 pi_lab sessionId",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
const response = await fetch(`${bridgeBaseUrl()}/api/page-ai/pi/ui-request-bridge`, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: {
|
|
|
|
|
"content-type": "application/json",
|
|
|
|
|
"x-mnote-pi-lab-bridge-token": BRIDGE_TOKEN,
|
|
|
|
|
"x-mnote-pi-lab-bridge-token": bridgeToken(),
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({ sessionId: SESSION_ID, ...request }),
|
|
|
|
|
body: JSON.stringify({ sessionId, ...request }),
|
|
|
|
|
});
|
|
|
|
|
return await response.json().catch(() => ({ ok: false, cancelled: true, code: "bad_json" }));
|
|
|
|
|
}
|
|
|
|
@@ -467,11 +778,11 @@ function register(pi: ExtensionAPI, tool: MnoteToolManifestEntry) {
|
|
|
|
|
}
|
|
|
|
|
const nativeResult = executeNativeTool(tool, params);
|
|
|
|
|
if (nativeResult) return nativeResult;
|
|
|
|
|
if (runtimeImplementation() === "pi-rust" && !HTTP_BRIDGE_AVAILABLE) {
|
|
|
|
|
if (runtimeImplementation() === "pi-rust" && !HTTP_BRIDGE_AVAILABLE && !bridgeToken()) {
|
|
|
|
|
return toolResult({
|
|
|
|
|
ok: false,
|
|
|
|
|
code: "mnote_pi_rust_service_bridge_unavailable",
|
|
|
|
|
message: `${tool.mnoteName} 仍依赖旧 HTTP bridge;Pi Rust 当前应通过原生文件工具或专用 MCP 扩展调用。`,
|
|
|
|
|
message: `${tool.mnoteName} 缺少 MNote Pi bridge token;Pi Rust 当前应通过原生文件工具或 MNote 受控 bridge 调用。`,
|
|
|
|
|
toolName: tool.mnoteName,
|
|
|
|
|
runtimeImplementation: runtimeImplementation(),
|
|
|
|
|
}, true);
|
|
|
|
@@ -487,7 +798,7 @@ async function executeBridgeTool(
|
|
|
|
|
ctx: ExtensionContext | undefined,
|
|
|
|
|
tool: MnoteToolManifestEntry,
|
|
|
|
|
) {
|
|
|
|
|
const nextParams = { ...((params || {}) as Record<string, unknown>) };
|
|
|
|
|
const nextParams = contextualToolParams(tool.mnoteName, params);
|
|
|
|
|
const approval = await requestMnoteApproval(ctx, String(toolCallId || ""), tool.mnoteName, tool.label, nextParams);
|
|
|
|
|
if (approval && approval.denied) {
|
|
|
|
|
return {
|
|
|
|
|