Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,15 @@ jobs:
run: |
mkdir -p lib

# 长截图算法层 C++ 对象(与 scripts/build-swift.sh
# 长截图 + 翻译聚类算法层 C++ 对象(与 scripts/build-swift.sh
# 逐字一致,两处编译参数必须同步修改。Apple clang 的 C++ 不支持 -Osize,
# 取等价的 -Oz)
LC_CXX_SOURCES=(
src/screenshot/algo/lc_match_core.cpp
src/screenshot/algo/lc_stitch_state.cpp
src/screenshot/macos/lc_bridge_mac.cpp
src/screenshot/algo/translate_cluster.cpp
src/screenshot/macos/translate_bridge_mac.cpp
)
LC_CXX_FLAGS=(-c -std=c++17 -Oz -DNDEBUG -I src/screenshot/algo)

Expand Down Expand Up @@ -77,6 +79,7 @@ jobs:
src/screenshot/macos/ScreenshotTextMac.swift
src/screenshot/macos/ScreenshotMosaicMac.swift
src/screenshot/macos/ScreenshotOutputMac.swift
src/screenshot/macos/ScreenshotTranslateMac.swift
src/screenshot/macos/ScreenshotLongCaptureMac.swift
src/screenshot/macos/ScreenshotLCPanelMac.swift
src/screenshot/macos/ScreenshotLCToolbarMac.swift
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ node_modules/
build/
*.dylib
*.log
*.obj
.DS_Store

# 构建产物:由 scripts/gen-icons.js 从 src/assets/*.svg 生成
Expand Down
227 changes: 217 additions & 10 deletions README.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions binding.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@
"src/screenshot/windows/annotations_windows.cpp",
"src/screenshot/windows/mosaic_windows.cpp",
"src/screenshot/windows/output_windows.cpp",
"src/screenshot/windows/translate_windows.cpp",
"src/screenshot/algo/lc_match_core.cpp",
"src/screenshot/algo/lc_stitch_state.cpp",
"src/screenshot/algo/translate_cluster.cpp",
"src/screenshot/windows/lc_frame_io_windows.cpp",
"src/screenshot/windows/lc_panel_ui_windows.cpp",
"src/screenshot/windows/lc_toolbar_ui_windows.cpp",
Expand Down
251 changes: 247 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,91 @@ const os = require('os');
const addon = require('./build/Release/ztools_native.node');
const platform = os.platform();

/**
* 原生层日志管理。
*
* 原生模块(N-API 绑定、截图会话、监控线程等)的关键调用、状态变化与失败路径
* 都会写入系统临时目录下的 ztools-native.log(不超过 10MB,写满后整体轮转为
* ztools-native.log.old)。通过本类可查询/修改日志等级、获取日志文件路径,
* 也可以让 JS 侧把日志写进同一个文件(统一时间线,便于与原生日志对齐排查)。
*
* 等级:trace < debug < info < warn < error(另有 off = 完全关闭)。
* 默认 info;可用环境变量 ZTOOLS_LOG_LEVEL(进程启动前设置)或 setLevel()
* (运行时覆盖)控制。
*/
class Logger {
/**
* 支持的日志等级列表
* @returns {string[]} ['trace', 'debug', 'info', 'warn', 'error', 'off']
*/
static get levels() {
return ['trace', 'debug', 'info', 'warn', 'error', 'off'];
}

/**
* 运行时设置日志输出等级(覆盖 ZTOOLS_LOG_LEVEL 环境变量)
* @param {string} level - 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'off'
* @throws {TypeError} level 不是合法等级字符串时抛出
*/
static setLevel(level) {
if (typeof level !== 'string' || !Logger.levels.includes(level.toLowerCase())) {
throw new TypeError(`level must be one of: ${Logger.levels.join(', ')}`);
}
addon.setLogLevel(level.toLowerCase());
}

/**
* 查询当前日志输出等级
* @returns {string} 当前等级字符串
*/
static getLevel() {
return addon.getLogLevel();
}

/**
* 查询某等级当前是否会被写入
* @param {string} level - 等级字符串
* @returns {boolean} 该等级日志当前是否输出
*/
static isLevelEnabled(level) {
if (typeof level !== 'string' || !Logger.levels.includes(level.toLowerCase())) {
throw new TypeError(`level must be one of: ${Logger.levels.join(', ')}`);
}
return addon.isLogLevelEnabled(level.toLowerCase());
}

/**
* 获取原生日志文件完整路径(系统临时目录下 ztools-native.log)
* @returns {string} 日志文件绝对路径
* @example
* console.log('原生日志:', Logger.getPath());
*/
static getPath() {
return addon.getLogFilePath();
}

/**
* 向原生日志文件写入一条日志(与原生日志同一文件、同一格式)
* @param {string} level - 'trace' | 'debug' | 'info' | 'warn' | 'error'
* @param {string} tag - 短模块名(如 'app')
* @param {string} message - 消息内容(不建议包含超大文本/敏感信息)
* @example
* Logger.write('info', 'app', '截图流程开始');
*/
static write(level, tag, message) {
if (typeof level !== 'string' || !Logger.levels.slice(0, 5).includes(level.toLowerCase())) {
throw new TypeError(`level must be one of: ${Logger.levels.slice(0, 5).join(', ')}`);
}
if (typeof tag !== 'string' || !tag) {
throw new TypeError('tag must be a non-empty string');
}
if (typeof message !== 'string') {
throw new TypeError('message must be a string');
}
addon.logWrite(level.toLowerCase(), tag, message);
}
}

class ClipboardMonitor {
constructor() {
this._callback = null;
Expand Down Expand Up @@ -494,6 +579,9 @@ class ScreenCapture {
* 拼接无帧数/像素上限,可持续合并至用户主动结束
* @param {number} [options.longCapture.interval=250] - 滚轮停止后等待内容稳定的毫秒数(50~2000,采样防抖;
* 滚动进行中也会按不低于 min(interval, 250)ms 的节拍主动采样,保证相邻帧有大重叠区域)
* 编辑态工具栏另有「翻译」按钮(Windows/macOS):OCR 识别选区文字 → 翻译 → 译文覆盖原文字区域,
* 依赖 ProviderBridge 注册默认启用的 ocr/translation provider(约定见 README「截图翻译」);
* 确认/保存导出的图像同样包含译文覆盖
* @param {Function} [callback] - 截图完成时的回调函数
* - 参数: { success: boolean, x?: number, y?: number, x2?: number, y2?: number, width?: number, height?: number, base64?: string, error?: string }
* - success: 是否成功截图
Expand All @@ -510,8 +598,11 @@ class ScreenCapture {
* 马赛克/撤销重做/选区圆角)、圆角透明导出、保存对话框与长截图全子系统均已实现,
* 行为与 Windows 版对齐;差异项见 README「平台差异」表
* - 额外需要辅助功能权限:ESC/右键兜底取消、长截图滚轮观察与 autoScroll(CGEventTap)
* - macOS 的 start() 会阻塞 JS 主线程直至会话收束(覆盖层事件循环运行在调用线程上),
* 会话期间无法用本进程定时器触发 abortLongCapture,需要时请从另一进程/线程调用
* - macOS 的 start() 为**非阻塞**调用:创建会话后立即返回,结果经 callback 异步送达;
* 会话期间 AppKit 事件与 Node/libuv 事件循环均由宿主进程自身的主事件循环驱动
* (Electron 主进程天然满足),会话期间可正常使用本进程定时器/异步逻辑
* (如定时触发 abortLongCapture)。纯 Node 宿主不驱动 macOS 主事件循环,
* 无法运行截图会话,请在 Electron 宿主中使用(见 README「平台差异」)
*
* @example
* // 默认:框选/点选完成即出图,不再二次编辑
Expand Down Expand Up @@ -545,8 +636,8 @@ class ScreenCapture {
/**
* 中止进行中的长截图滚动捕获(Windows / macOS 双平台)
* 滚动捕获会以失败结果(success: false)回调后结束(ESC/取消同语义:取消 = 失败收束);
* 无进行中的长截图时为安全空操作。可在任意线程/进程调用(macOS 会话期间 JS 主线程
* 被阻塞时,从另一进程或工作线程调用即可,见 test/test-screenshot-mac.js 的注入示例)
* 无进行中的长截图时为安全空操作,可在任意线程/进程调用(macOS start() 为非阻塞
* 调用,会话期间本进程定时器/异步逻辑照常运转,可直接调用本方法)
*/
static abortLongCapture() {
addon.abortLongCapture();
Expand Down Expand Up @@ -729,8 +820,159 @@ function launchCuiShell(shell, currentDirectory) {
return addon.launchCuiShell(shell, currentDirectory);
}

/**
* Provider 桥接层:让原生层(任意 native 线程上的 C++/Swift 代码)调用 JS 侧注册的方法。
*
* 典型用法(宿主侧,如 ZTools 主进程把 provider 能力交给原生层):
* const { ProviderBridge } = require('ztools-native-api');
* ProviderBridge.start(async (type, input) => {
* // type: 'translation' | 'ocr' 等(由宿主与原生侧自行约定)
* // 返回值会以 JSON 序列化后回传给原生线程
* return await providerManager.invoke(type, input);
* });
*
* 内置约定(截图工具栏「翻译」按钮依赖以下两个类型,详见 README「截图翻译」):
* - ocr: { image } => { text, blocks: [{ text, left, top, right, bottom }] }
* (blocks 为行级坐标块;provider 只回整段 text 时原生自动走整图兜底)
* - translation: { text } => { text }(OCR 多行结果由原生按聚类段落分别调用)
*
* 原生侧(C++/Swift)调用方式见 src/provider_bridge.h 的
* ztools_provider_bridge::Invoke(type, inputJson, timeoutMs)。
*
* 注意:原生侧的 Invoke 严禁在 JS 主线程上调用(内部检测并直接报错),事件回调
* 线程、消息循环线程等 native 线程均可安全调用。
*/
const ProviderBridge = {
/**
* 启动桥接,注册 JS 侧 handler。重复启动会抛错;stop 之后可重新 start。
* @param {Function} handler - (type: string, input: any) => Promise<any>;
* 返回值(或 reject 的错误信息)会回传给发起调用的原生线程
* @returns {void} 无返回值
* @throws {TypeError} handler 不是函数时抛出
* @throws {Error} 桥接已启动时抛出
*/
start(handler) {
if (typeof handler !== 'function') {
throw new TypeError('ProviderBridge.start requires a handler function');
}
addon.startProviderBridge((type, inputJson, seq) => {
// handler 同步启动:分发即开始执行(含同步抛错经 reject 回传),异步续体由
// 宿主事件循环的常规回调边界推进并排空 microtask
let result;
try {
result = handler(type, JSON.parse(inputJson));
} catch (err) {
addon.rejectProviderBridge(seq, err instanceof Error ? err.message : String(err));
return;
}
Promise.resolve(result)
.then((value) => {
addon.resolveProviderBridge(seq, JSON.stringify(value === undefined ? null : value));
})
.catch((err) => {
addon.rejectProviderBridge(seq, err instanceof Error ? err.message : String(err));
});
});
},

/**
* 停止桥接并丢弃 handler。已在等待的原生调用会立即收到 "provider bridge stopped" 错误。
* @returns {void} 无返回值
*/
stop() {
addon.stopProviderBridge();
},

/**
* 查询桥接是否就绪。
* @returns {boolean} 已 start 且未 stop 时为 true
*/
isReady() {
return addon.isProviderBridgeReady();
},

/**
* 从 JS 侧发起一次"原生发起"的调用:走真实原生线程 → JS 线程的完整通路,
* 用于验证桥接链路(原生业务代码应直接调用 C++ 的 Invoke,而不是本方法)。
* @param {string} type - 能力类型
* @param {any} input - 入参(自动 JSON 序列化传给 handler)
* @param {number} [timeoutMs=15000] - 原生侧等待结果的超时毫秒数
* @returns {Promise<any>} handler 的返回值;超时或 handler 抛错时 Promise 被 reject
*/
invokeFromNative(type, input, timeoutMs = 15000) {
return new Promise((resolve, reject) => {
addon.invokeProviderFromNative(
type,
JSON.stringify(input === undefined ? null : input),
timeoutMs,
(err, resultJson) => {
if (err) {
reject(new Error(err));
return;
}
try {
resolve(JSON.parse(resultJson));
} catch (parseErr) {
reject(parseErr);
}
}
);
});
},

/**
* 从 JS 侧发起一次"原生发起"的异步调用:走与 macOS 截图翻译完全相同的异步通路
* (原生 InvokeAsync + requestId + 看门狗超时 + CancelAsync 丢弃晚到结果),
* 用于验证异步桥接(原生业务代码应直接调用 C++ 的 InvokeAsync,而不是本方法)。
* @param {string} type - 能力类型
* @param {any} input - 入参(自动 JSON 序列化传给 handler)
* @param {number} requestId - 请求号(调用方保证唯一;用于 cancelFromNative 取消)
* @param {number} [timeoutMs=15000] - 超时毫秒数(到时取消请求并 reject;handler
* 晚到的 resolve/reject 被原生侧丢弃)
* @returns {Promise<any>} handler 的返回值;桥接未就绪/超时/handler 抛错时 reject
*/
invokeFromNativeAsync(type, input, requestId, timeoutMs = 15000) {
return new Promise((resolve, reject) => {
let settled = false;
const finish = (fn, arg) => {
if (settled) return; // 晚到结果(已超时/已取消)静默丢弃
settled = true;
fn(arg);
};
addon.invokeProviderAsyncFromNative(
type,
JSON.stringify(input === undefined ? null : input),
requestId,
timeoutMs,
(err, resultJson) => {
if (err) {
finish(reject, new Error(err));
return;
}
try {
finish(resolve, JSON.parse(resultJson));
} catch (parseErr) {
finish(reject, parseErr);
}
}
);
});
},

/**
* 取消一个经 invokeFromNativeAsync 发起的请求(幂等;已回调/已取消时为 false)。
* 取消后 handler 晚到的 resolve/reject 被原生侧按 requestId 丢弃。
* @param {number} requestId - 请求号
* @returns {boolean} 是否确由本次调用完成取消
*/
cancelFromNative(requestId) {
return addon.cancelProviderAsyncFromNative(requestId);
}
};

// 导出所有类
module.exports = {
Logger,
ClipboardMonitor,
WindowMonitor,
WindowManager,
Expand All @@ -741,6 +983,7 @@ module.exports = {
UwpManager,
MuiResolver,
WindowsShortcutScanner,
ProviderBridge,
getSelectedContent,
launchCuiShell
};
Expand Down
9 changes: 7 additions & 2 deletions scripts/build-swift.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,18 @@ echo "📱 当前架构: $ARCH"
# 长截图算法层 C++ 对象(lc_match_core + lc_stitch_state
# 纯算法层与 lc_bridge C ABI shim,clang++ 编出 .o 后随 swiftc -emit-library 链入
# dylib;Swift 侧经 src/screenshot/macos/LCBridgeMac.swift 的 @_silgen_name 调用。
# 翻译聚类算法(translate_cluster)与其 C ABI shim 同链路(ScreenshotTranslateMac.swift
# 经 @_silgen_name 调用)。
# 注意:本段与 .github/workflows/build.yml「Build Swift library」步骤内联实现逐字一致
# (两处编译参数必须同步修改)。Apple clang 的 C++ 不支持 -Osize,取等价的 -Oz。
# -I:算法层头文件目录(lc_bridge_mac.cpp 位于 macos/,需检索 ../algo 的
# long_capture_internal.h / lc_platform.h)。
# -I:算法层头文件目录(lc_bridge_mac.cpp / translate_bridge_mac.cpp 位于 macos/,
# 需检索 ../algo 的 long_capture_internal.h / translate_cluster_internal.h / lc_platform.h)。
LC_CXX_SOURCES=(
src/screenshot/algo/lc_match_core.cpp
src/screenshot/algo/lc_stitch_state.cpp
src/screenshot/macos/lc_bridge_mac.cpp
src/screenshot/algo/translate_cluster.cpp
src/screenshot/macos/translate_bridge_mac.cpp
)
LC_CXX_FLAGS=(-c -std=c++17 -Oz -DNDEBUG -I src/screenshot/algo)

Expand Down Expand Up @@ -63,6 +67,7 @@ SWIFT_SOURCES=(
src/screenshot/macos/ScreenshotTextMac.swift
src/screenshot/macos/ScreenshotMosaicMac.swift
src/screenshot/macos/ScreenshotOutputMac.swift
src/screenshot/macos/ScreenshotTranslateMac.swift
src/screenshot/macos/ScreenshotLongCaptureMac.swift
src/screenshot/macos/ScreenshotLCPanelMac.swift
src/screenshot/macos/ScreenshotLCToolbarMac.swift
Expand Down
Loading
Loading