diff --git a/.gitignore b/.gitignore deleted file mode 100644 index cb2ae62..0000000 --- a/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -__pycache__/ -*.pyc -.DS_Store -node_modules/ \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 7203af6..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 shuiyu - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/README.md b/README.md index 4e5693f..ab48186 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,7 @@ 你好呀~这个仓库已经搬到更好用的地方啦。 -为了让代码管理更清爽、多平台同步更省心,我们把原先分散的镜像统一收拢到 -**Gitee** 与 **GitHub** 主仓维护,这里就只保留这张"迁移小纸条"咯。 +为了让代码管理更清爽、多平台同步更省心,我们把原先分散的镜像统一收拢到主仓维护,这里就只保留这张"迁移小纸条"咯。 📦 新家在这里: - Gitee:https://gitee.com/shuiyu1123/xiaoli-cli-operit-plugins diff --git a/operit_bridge.py b/operit_bridge.py deleted file mode 100644 index 3e6934d..0000000 --- a/operit_bridge.py +++ /dev/null @@ -1,922 +0,0 @@ -""" -Operit Bridge — xiaoli-cli 原生 Liugin 插件 -=========================================== -让 xiaoli-cli 兼容 Operit AI 的第三方工具格式。 - -自动扫描 plugins/operit/ 子目录下所有 .js/.ts 脚本, -解析 METADATA 块,注册为 xiaoli-cli 可调用的工具。 -""" -import os -import re -import json -import glob -import importlib.util -from typing import Dict, List, Optional - - -# ═══════════════════════════════════════════════════════════════ -# Operit METADATA 解析器 -# ═══════════════════════════════════════════════════════════════ - -def _hjson_to_json(raw: str) -> str: - """ - 将 Operit 的 HJSON 风格元数据转为标准 JSON。 - 逐行解析,处理: 无引号键名、单引号/三引号字符串、嵌套双引号、// 注释、尾逗号。 - """ - SQ = chr(39) # ' - DQ = chr(34) # " - result = [] - i = 0 - n = len(raw) - - def skip_ws(): - nonlocal i - while i < n and raw[i] in ' \t\r\n': - i += 1 - - def parse_string_val(quote): - """解析引号字符串,正确处理转义""" - nonlocal i - i += 1 # skip opening quote - chars = [] - while i < n: - ch = raw[i] - if ch == '\\': - chars.append(ch) - i += 1 - if i < n: - chars.append(raw[i]) - i += 1 - elif ch == quote: - i += 1 # skip closing quote - return ''.join(chars) - else: - chars.append(ch) - i += 1 - return ''.join(chars) # unclosed - - def parse_triple_string(): - """解析 '''...''' 三引号字符串""" - nonlocal i - i += 3 # skip ''' - chars = [] - while i < n - 2: - if raw[i:i+3] == SQ * 3: - i += 3 - return ''.join(chars) - chars.append(raw[i]) - i += 1 - return ''.join(chars) - - def parse_unquoted_val(): - """解析无引号值,到行尾或逗号/}等""" - nonlocal i - chars = [] - depth = 0 - while i < n: - ch = raw[i] - if ch == '{' or ch == '[': - depth += 1 - chars.append(ch) - i += 1 - elif ch == '}' or ch == ']': - if depth <= 0: - break - depth -= 1 - chars.append(ch) - i += 1 - elif ch == ',' and depth == 0: - break - elif ch == '\n' and depth == 0: - break - else: - chars.append(ch) - i += 1 - return ''.join(chars).strip() - - def escape_for_json(s): - """转义字符串为 JSON 安全格式""" - return json.dumps(s) - - # ── 主解析循环 ── - skip_ws() - if i >= n or raw[i] != '{': - return raw # 不是对象 - result.append('{') - i += 1 - - expect_comma = False - while i < n: - skip_ws() - if i >= n: - break - ch = raw[i] - - # 对象结束 - if ch == '}': - result.append('}') - i += 1 - break - # 数组结束 - if ch == ']': - result.append(']') - i += 1 - break - # 逗号 - if ch == ',': - result.append(',') - i += 1 - expect_comma = False - continue - - # 注释 - if ch == '/' and i + 1 < n and raw[i+1] == '/': - while i < n and raw[i] != '\n': - i += 1 - continue - - # 如果需要逗号但遇到新键/值,自动补逗号 - if expect_comma and (ch == '"' or ch == SQ or ch.isalpha() or ch == '_' or ch == '{' or ch == '['): - result.append(',') - expect_comma = False - - # 解析 key - key = None - if ch == '"': - key = parse_string_val('"') - elif ch == SQ: - if i + 2 < n and raw[i+1] == SQ and raw[i+2] == SQ: - key = parse_triple_string() - else: - key = parse_string_val(SQ) - elif ch.isalpha() or ch == '_': - # 无引号键名 - start = i - while i < n and (raw[i].isalnum() or raw[i] == '_'): - i += 1 - key = raw[start:i] - - if key is None: - i += 1 - continue - - skip_ws() - if i >= n or raw[i] != ':': - continue - i += 1 # skip ':' - skip_ws() - - # 输出 key - result.append(escape_for_json(key)) - result.append(':') - - # 解析 value - if i >= n: - break - vch = raw[i] - - if vch == '"': - # 双引号字符串 - val = parse_string_val('"') - result.append(escape_for_json(val)) - elif vch == SQ: - if i + 2 < n and raw[i+1] == SQ and raw[i+2] == SQ: - val = parse_triple_string() - else: - val = parse_string_val(SQ) - result.append(escape_for_json(val)) - elif vch == '{' or vch == '[': - # 嵌套对象/数组 — 递归处理 - if vch == '{': - depth = 1 - j = i + 1 - while j < n and depth > 0: - if raw[j] == '{': depth += 1 - elif raw[j] == '}': depth -= 1 - j += 1 - inner = raw[i:j] - result.append(_hjson_to_json(inner)) - i = j - else: - # 数组 — 找匹配的 ],对内部每个元素递归 - depth = 1 - j = i + 1 - while j < n and depth > 0: - if raw[j] == '[': depth += 1 - elif raw[j] == ']': depth -= 1 - j += 1 - arr_inner = raw[i+1:j-1].strip() - result.append('[') - # 按顶层逗号分割数组元素 - elems = [] - elem_start = 0 - d = 0 - for k, c in enumerate(arr_inner): - if c in '{[': d += 1 - elif c in '}]': d -= 1 - elif c == ',' and d == 0: - elems.append(arr_inner[elem_start:k]) - elem_start = k + 1 - elems.append(arr_inner[elem_start:]) - for ei, elem in enumerate(elems): - elem = elem.strip() - if not elem: - continue - if ei > 0: - result.append(',') - if elem.startswith('{'): - result.append(_hjson_to_json(elem)) - elif elem.startswith('['): - # 嵌套数组 - result.append(_hjson_to_json('{"_":' + elem + '}')[6:-1]) - else: - # 普通值 - if elem.startswith('"') or elem.startswith(SQ): - q = elem[0] - if len(elem) >= 3 and elem[1] == q and elem[2] == q: - val = elem[3:-3] - else: - val = elem[1:-1] - result.append(json.dumps(val)) - elif elem in ('true', 'false', 'null'): - result.append(elem) - else: - try: - float(elem) - result.append(elem) - except ValueError: - result.append(json.dumps(elem)) - result.append(']') - i = j - elif vch in ('t', 'f', 'n') and raw[i:i+4] in ('true', 'null'): - result.append(raw[i:i+4]) - i += 4 - elif vch == 'f' and raw[i:i+5] == 'false': - result.append('false') - i += 5 - elif vch.isdigit() or vch == '-': - start = i - while i < n and (raw[i].isdigit() or raw[i] in '.-eE+'): - i += 1 - result.append(raw[start:i]) - else: - # 无引号字符串值 - val = parse_unquoted_val() - if val: - result.append(escape_for_json(val)) - - expect_comma = True - - return ''.join(result) - - -def parse_operit_metadata(file_path: str) -> Optional[Dict]: - """ - 从 JS/TS 文件中提取 /* METADATA ... */ 块并解析为 dict。 - 支持 Operit 完整 schema: name, display_name, description, author, category, env, tools, states - 同时兼容标准 JSON 和 HJSON 风格(无引号键名、单引号、三引号、注释)。 - """ - try: - with open(file_path, "r", encoding="utf-8") as f: - content = f.read() - except (FileNotFoundError, UnicodeDecodeError): - return None - - # 匹配 /* METADATA ... */ 块 - pattern = r'/\*\s*METADATA\s*\n?(.*?)\*/' - match = re.search(pattern, content, re.DOTALL) - if not match: - return None - - raw = match.group(1).strip() - - # 先尝试标准 JSON - try: - return json.loads(raw) - except json.JSONDecodeError: - pass - - # 尝试 HJSON 转换 - try: - cleaned = _hjson_to_json(raw) - return json.loads(cleaned) - except json.JSONDecodeError: - # 最后兜底: 移除注释和尾逗号再试 - fallback = re.sub(r'//.*$', '', raw, flags=re.MULTILINE) - fallback = re.sub(r',\s*([}\]])', r'\1', fallback) - try: - return json.loads(fallback) - except json.JSONDecodeError: - return None - - -def _localized(field, lang="zh") -> str: - """解析 Operit 多语言字段: 字符串 或 {zh, en, default} 对象""" - if isinstance(field, str): - return field - if isinstance(field, dict): - for key in [lang, lang.split("-")[0], "default"]: - if key in field: - return field[key] - return next(iter(field.values()), "") - return str(field) if field else "" - - -# ═══════════════════════════════════════════════════════════════ -# 格式转换: Operit ↔ MCP -# ═══════════════════════════════════════════════════════════════ - -_TYPE_MAP = { - "string": "string", "number": "number", "integer": "integer", - "boolean": "boolean", "object": "object", "array": "array", -} - - -def operit_tool_to_mcp(tool_def: Dict, script_name: str) -> Dict: - """Operit tool 定义 → MCP inputSchema 格式""" - properties, required = {}, [] - for p in tool_def.get("parameters", []): - pname = p.get("name", "") - properties[pname] = { - "type": _TYPE_MAP.get(p.get("type", "string"), "string"), - "description": p.get("description", ""), - } - if p.get("required", False): - required.append(pname) - - full_name = f"{script_name}__{tool_def.get('name', 'unknown')}" - return { - "name": full_name, - "description": _localized(tool_def.get("description", "")), - "inputSchema": { - "type": "object", - "properties": properties, - "required": required, - }, - } - - -def meta_to_tools(meta: Dict) -> List[Dict]: - """一个 Operit METADATA → xiaoli-cli 工具列表""" - script_name = meta.get("name", "unknown") - category = meta.get("category", "") - tools = [] - for td in meta.get("tools", []): - mcp = operit_tool_to_mcp(td, script_name) - tools.append({ - "name": mcp["name"], - "description": mcp["description"], - "keywords": _build_keywords(meta, td), - "usage": _build_usage(mcp), - "mcp_definition": mcp, - "source": "operit", - "script_name": script_name, - "original_tool_name": td.get("name", ""), - }) - return tools - - -def _build_keywords(meta: Dict, tool_def: Dict) -> List[str]: - kw = ["operit"] - for s in [meta.get("name", ""), tool_def.get("name", ""), meta.get("category", "")]: - if s and s not in kw: - kw.append(s) - for text in [_localized(meta.get("description", "")), _localized(tool_def.get("description", ""))]: - for w in re.findall(r'[\u4e00-\u9fff]+|[a-zA-Z]{3,}', text): - if w not in kw: - kw.append(w) - return kw - - -def _build_usage(mcp: Dict) -> str: - props = mcp.get("inputSchema", {}).get("properties", {}) - req = mcp.get("inputSchema", {}).get("required", []) - lines = [f"Operit 工具: {mcp['name']}", mcp.get("description", ""), ""] - if props: - lines.append("参数:") - for pn, pd in props.items(): - r = " (必填)" if pn in req else "" - lines.append(f" {pn}: {pd.get('description', pd.get('type', ''))}{r}") - lines.append(f'\n调用: {{"action":"use_tool","tool":"{mcp["name"]}","args":"..."}}') - return "\n".join(lines) - - -def xiaoli_to_operit_js(plugin_instance) -> str: - """将 xiaoli-cli 插件导出为 Operit METADATA JS 文件内容""" - info = plugin_instance.get_tool_info() - mcp = plugin_instance.get_mcp_definition() - name = info.get("name", "Unknown") - desc = info.get("description", "") - schema = mcp.get("inputSchema", {}) - props = schema.get("properties", {}) - required = schema.get("required", []) - - parameters = [] - for pn, pd in props.items(): - parameters.append({ - "name": pn, - "description": pd.get("description", ""), - "type": pd.get("type", "string"), - "required": pn in required, - }) - - metadata = { - "name": name, - "display_name": {"zh": desc[:50], "en": name}, - "description": {"zh": desc, "en": desc}, - "author": ["xiaoli-cli"], - "category": "Utility", - "tools": [{ - "name": "execute", - "description": {"zh": f"执行 {name}", "en": f"Execute {name}"}, - "parameters": parameters or [ - {"name": "args", "description": "操作参数", "type": "string", "required": True} - ], - }], - } - - meta_json = json.dumps(metadata, indent=4, ensure_ascii=False) - lines = [ - "/*", "METADATA", meta_json, "*/", "", - f"// Auto-generated from xiaoli-cli plugin: {name}", - f"// Compatible with Operit AI tool system", "", - f"const {name}Bridge = (function () {{", - " async function wrap(func, params) {", - " try { return await func(params); }", - ' catch (e) { return { success: false, message: "执行失败: " + e.message }; }', - " }", - " async function execute(params) {", - f" // 由 xiaoli-cli {name} 插件提供", - ' const args = params.args || JSON.stringify(params);', - f' const cmd = "cd /path/to/xiaoli-cli && python3 -c \\"' - f"import sys; sys.path.insert(0, '.'); " - f"from plugins.{name} import Liugin; p = Liugin(); " - f"""print(p.handle('\\" + args + \\"'))\\\\"";""", - " try { return { success: true, data: await Tools.System.execute(cmd) }; }", - " catch (e) { return { success: false, message: e.message }; }", - " }", - " return { execute: function(p) { return wrap(execute, p); } };", - "})();", - f"exports.execute = {name}Bridge.execute;", - ] - return "\n".join(lines) - - -# ═══════════════════════════════════════════════════════════════ -# Liugin 插件主体 -# ═══════════════════════════════════════════════════════════════ - -class Liugin: - """ - Operit Bridge — 让 xiaoli-cli 兼容 Operit AI 第三方工具格式。 - - 自动扫描 plugins/operit/ 子目录,解析所有 Operit METADATA 脚本, - 注册为 xiaoli-cli 可调用的工具。支持双向格式转换和导出。 - """ - - def __init__(self): - self.cli = None - # 已加载的 Operit 工具: name -> tool_info - self._operit_tools: Dict[str, Dict] = {} - # 脚本源文件映射 - self._script_files: Dict[str, str] = {} - - def set_cli(self, cli): - self.cli = cli - # 挂载后自动扫描 operit 子目录 - self._auto_scan() - - # ── 自动扫描 plugins/operit/ ── - - def _get_operit_dir(self) -> str: - """获取 plugins/operit/ 绝对路径""" - plugin_dir = os.path.dirname(os.path.abspath(__file__)) - return os.path.join(plugin_dir, "operit") - - def _auto_scan(self): - """启动时自动扫描 operit 子目录""" - operit_dir = self._get_operit_dir() - if not os.path.isdir(operit_dir): - return - count = 0 - for ext in ("*.js", "*.ts"): - for fpath in glob.glob(os.path.join(operit_dir, "**", ext), recursive=True): - meta = parse_operit_metadata(fpath) - if meta: - for t in meta_to_tools(meta): - t["_source_file"] = fpath - self._operit_tools[t["name"]] = t - self._script_files[meta.get("name", "")] = fpath - count += 1 - if count: - print(f"[operit_bridge] 自动加载了 {count} 个 Operit 脚本, " - f"{len(self._operit_tools)} 个工具") - - # ── Liugin 协议接口 ── - - def get_tool_info(self): - return { - "name": "operit_bridge", - "description": "Operit 双向工具桥接 — 自动加载 plugins/operit/ 下的 Operit 脚本, " - "支持解析/扫描/导出/格式转换", - "keywords": [ - "operit", "bridge", "桥接", "兼容", "格式转换", "METADATA", - "插件互通", "第三方工具", "脚本", "导入", "导出", - ], - "usage": ( - "operit_bridge <操作> [参数]\n" - "操作:\n" - " scan [目录] - 扫描 Operit 脚本目录 (默认 plugins/operit/)\n" - " list - 列出已加载的 Operit 工具\n" - " parse <文件> - 解析单个 Operit METADATA 文件\n" - " info <工具名> - 查看工具详情\n" - " export <插件名> - 将 xiaoli-cli 插件导出为 Operit 格式\n" - " export_all [目录] - 导出所有插件\n" - " convert_mcp - MCP → Operit\n" - " convert_operit - Operit → MCP" - ), - } - - def get_mcp_definition(self): - return { - "name": "operit_bridge", - "description": "Operit 双向工具桥接,自动加载 plugins/operit/ 目录下的 Operit 格式脚本", - "inputSchema": { - "type": "object", - "properties": { - "operation": { - "type": "string", - "enum": ["scan", "list", "parse", "info", - "export", "export_all", "convert_mcp", "convert_operit"], - "description": "操作类型", - }, - "target": { - "type": "string", - "description": "目标路径/插件名/JSON", - }, - }, - "required": ["operation"], - }, - } - - def convert_mcp_args(self, arguments): - op = arguments.get("operation", "") - target = arguments.get("target", "") - return f"{op} {target}".strip() - - def handle(self, args: str) -> str: - parts = args.strip().split(maxsplit=1) - if not parts: - return self._help() - op = parts[0].lower() - target = parts[1].strip() if len(parts) > 1 else "" - handler = { - "scan": self._h_scan, "list": self._h_list, - "parse": self._h_parse, "info": self._h_info, - "export": self._h_export, "export_all": self._h_export_all, - "convert_mcp": self._h_convert_mcp, "convert_operit": self._h_convert_operit, - }.get(op) - if not handler: - return f"错误: 不支持的操作 '{op}'\n{self._help()}" - try: - return handler(target) - except Exception as e: - return f"Operit Bridge 错误: {e}" - - def _help(self): - return ( - "Operit Bridge — 可用操作:\n" - " scan [目录] - 扫描 Operit 脚本\n" - " list - 列出已加载工具\n" - " parse <文件> - 解析单个 METADATA 文件\n" - " info <工具名> - 查看工具详情\n" - " export <插件名> - 导出 xiaoli 插件为 Operit 格式\n" - " export_all [目录] - 批量导出\n" - " convert_mcp - MCP → Operit\n" - " convert_operit - Operit → MCP" - ) - - # ═══════════════════════════════════════════════════════════ - # 操作实现 - # ═══════════════════════════════════════════════════════════ - - def _h_scan(self, target: str) -> str: - """扫描 Operit 脚本目录""" - operit_dir = target or self._get_operit_dir() - if not os.path.isdir(operit_dir): - return f"错误: 目录不存在 — {operit_dir}\n请将 Operit 脚本放入 plugins/operit/ 目录" - - found, tool_count = 0, 0 - for ext in ("*.js", "*.ts"): - for fpath in glob.glob(os.path.join(operit_dir, "**", ext), recursive=True): - meta = parse_operit_metadata(fpath) - if meta: - tools = meta_to_tools(meta) - for t in tools: - t["_source_file"] = fpath - self._operit_tools[t["name"]] = t - self._script_files[meta.get("name", "")] = fpath - found += 1 - tool_count += len(tools) - - lines = [f"✓ 扫描完成: {operit_dir}", f" {found} 个脚本, {tool_count} 个工具", ""] - for name, fpath in self._script_files.items(): - lines.append(f" 📦 {name} — {os.path.basename(fpath)}") - return "\n".join(lines) - - def _h_list(self, _: str) -> str: - """列出已加载的 Operit 工具""" - if not self._operit_tools: - return ("暂无已加载的 Operit 工具。\n" - "请将 .js/.ts 脚本放入 plugins/operit/ 目录,或使用 scan <目录> 手动加载。") - lines = [f"已加载 {len(self._operit_tools)} 个 Operit 工具:", ""] - for name, t in self._operit_tools.items(): - src = os.path.basename(t.get("_source_file", "?")) - lines.append(f" 🔧 {name} [{src}]") - desc = t.get("description", "") - if desc: - lines.append(f" {desc[:80]}") - return "\n".join(lines) - - def _h_parse(self, target: str) -> str: - """解析单个 Operit METADATA 文件""" - if not target: - return "错误: 请提供文件路径" - if not os.path.isfile(target): - return f"错误: 文件不存在 — {target}" - meta = parse_operit_metadata(target) - if not meta: - return f"错误: 未找到有效 METADATA 块 — {target}" - - tools = meta_to_tools(meta) - for t in tools: - t["_source_file"] = target - self._operit_tools[t["name"]] = t - - lines = [ - f"✓ 解析成功: {target}", - f" 脚本: {meta.get('name', 'N/A')}", - f" 描述: {_localized(meta.get('description', ''))}", - f" 分类: {meta.get('category', 'N/A')}", - ] - author = meta.get("author", []) - if author: - lines.append(f" 作者: {', '.join(author) if isinstance(author, list) else author}") - lines.append(f" 工具数: {len(tools)}") - lines.append("") - for t in tools: - mcp = t["mcp_definition"] - props = mcp["inputSchema"]["properties"] - req = mcp["inputSchema"]["required"] - lines.append(f" 🔧 {t['name']}") - lines.append(f" {t['description']}") - if props: - ps = [] - for pn, pd in props.items(): - r = "*" if pn in req else "" - ps.append(f"{pn}({pd.get('type', '?')}){r}") - lines.append(f" 参数: {', '.join(ps)}") - return "\n".join(lines) - - def _h_info(self, target: str) -> str: - """查看工具详情""" - if not target: - return "错误: 请提供工具名" - t = self._operit_tools.get(target) - if not t: - return f"错误: 未找到工具 '{target}'\n可用: {', '.join(self._operit_tools.keys()) or '无'}" - lines = [ - f"工具: {t['name']}", - f"描述: {t.get('description', '')}", - f"来源: {t.get('_source_file', 'unknown')}", - f"脚本: {t.get('script_name', 'N/A')}", - f"原始名: {t.get('original_tool_name', 'N/A')}", - f"关键词: {', '.join(t.get('keywords', []))}", - "", "MCP 定义:", - json.dumps(t.get("mcp_definition", {}), indent=2, ensure_ascii=False), - ] - return "\n".join(lines) - - def _h_export(self, target: str) -> str: - """将 xiaoli-cli 插件导出为 Operit 格式""" - if not target: - return "错误: 请提供插件名" - instance = self._load_xiaoli_plugin(target) - if isinstance(instance, str): - return instance # error msg - - js = xiaoli_to_operit_js(instance) - out_dir = os.path.join(self._get_operit_dir(), "exported") - os.makedirs(out_dir, exist_ok=True) - out_path = os.path.join(out_dir, f"{target}.js") - with open(out_path, "w", encoding="utf-8") as f: - f.write(js) - return f"✓ 导出成功!\n 输出: {out_path}\n 可直接放入 Operit AI 使用" - - def _h_export_all(self, target: str) -> str: - """批量导出所有 xiaoli 插件""" - plugins_dir = os.path.dirname(os.path.abspath(__file__)) - out_dir = target or os.path.join(self._get_operit_dir(), "exported") - os.makedirs(out_dir, exist_ok=True) - exported, errors = [], [] - for fname in sorted(os.listdir(plugins_dir)): - if not fname.endswith(".py") or fname == "__init__.py": - continue - name = fname[:-3] - try: - spec = importlib.util.spec_from_file_location(name, os.path.join(plugins_dir, fname)) - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - cls = getattr(mod, 'Liugin', None) or getattr(mod, 'Plugin', None) - if cls: - inst = cls() - with open(os.path.join(out_dir, f"{name}.js"), "w", encoding="utf-8") as f: - f.write(xiaoli_to_operit_js(inst)) - exported.append(name) - except Exception as e: - errors.append(f"{name}: {e}") - lines = [f"✓ 批量导出完成", f" 目录: {out_dir}", f" 成功: {len(exported)}"] - if errors: - lines.append(f" 失败: {len(errors)}") - for err in errors[:5]: - lines.append(f" ⚠ {err}") - return "\n".join(lines) - - def _h_convert_mcp(self, target: str) -> str: - """MCP → Operit METADATA""" - if not target: - return "错误: 请提供 MCP JSON" - mcp = json.loads(target) - name = mcp.get("name", "unknown") - desc = mcp.get("description", "") - props = mcp.get("inputSchema", {}).get("properties", {}) - required = mcp.get("inputSchema", {}).get("required", []) - params = [] - for pn, pd in props.items(): - params.append({"name": pn, "description": pd.get("description", ""), - "type": pd.get("type", "string"), "required": pn in required}) - operit = { - "name": name, - "display_name": {"zh": desc[:50] or name, "en": name}, - "description": {"zh": desc, "en": desc}, - "author": ["xiaoli-cli"], - "category": "Utility", - "tools": [{"name": "execute", "description": {"zh": f"执行 {name}", "en": f"Execute {name}"}, - "parameters": params}], - } - return json.dumps(operit, indent=2, ensure_ascii=False) - - def _h_convert_operit(self, target: str) -> str: - """Operit → MCP""" - if not target: - return "错误: 请提供 Operit METADATA JSON" - meta = json.loads(target) - results = [operit_tool_to_mcp(t, meta.get("name", "unknown")) for t in meta.get("tools", [])] - return json.dumps(results, indent=2, ensure_ascii=False) - - # ── 辅助 ── - - def _load_xiaoli_plugin(self, name: str): - """加载一个 xiaoli-cli 插件实例,返回实例或错误字符串""" - plugins_dir = os.path.dirname(os.path.abspath(__file__)) - fpath = os.path.join(plugins_dir, f"{name}.py") - if not os.path.isfile(fpath): - # 按 name 搜索 - for fname in os.listdir(plugins_dir): - if fname.endswith(".py") and fname != "__init__.py": - try: - spec = importlib.util.spec_from_file_location(fname[:-3], - os.path.join(plugins_dir, fname)) - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - cls = getattr(mod, 'Liugin', None) or getattr(mod, 'Plugin', None) - if cls: - inst = cls() - if inst.get_tool_info().get("name", "").lower() == name.lower(): - return inst - except Exception: - continue - return f"错误: 未找到插件 '{name}'" - try: - spec = importlib.util.spec_from_file_location(name, fpath) - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - cls = getattr(mod, 'Liugin', None) or getattr(mod, 'Plugin', None) - if not cls: - return f"错误: 插件文件中未找到 Liugin/Plugin 类" - return cls() - except Exception as e: - return f"错误: 加载失败 — {e}" - - # ── 给 plugin_manager 用的工具注册接口 ── - - def get_operit_tools_for_registration(self) -> List[Dict]: - """ - 返回所有已加载的 Operit 工具,格式兼容 plugin_manager.tools 列表。 - 可被 cli_base 在初始化时调用,将 Operit 工具注入工具列表。 - """ - result = [] - for name, t in self._operit_tools.items(): - result.append({ - "name": name, - "description": t.get("description", ""), - "keywords": t.get("keywords", []), - "usage": t.get("usage", ""), - "handler": self._make_handler(name), - "source": "operit", - }) - return result - - def _make_handler(self, tool_name: str): - """为 Operit 工具创建 handler 闭包""" - tool_info = self._operit_tools.get(tool_name) - if not tool_info: - return lambda args: f"错误: 工具 {tool_name} 未加载" - - def handler(args: str) -> str: - # 解析参数: 支持 JSON 和 "key=value" 两种格式 - params = {} - args = args.strip() - if args.startswith("{"): - try: - params = json.loads(args) - except json.JSONDecodeError: - params = {"args": args} - else: - # "city=北京 days=3" 格式 - for part in re.split(r'[;\s]+', args): - if '=' in part: - k, v = part.split('=', 1) - params[k.strip()] = v.strip() - elif part: - # 单个值填入第一个参数 - schema_props = tool_info["mcp_definition"]["inputSchema"]["properties"] - for pname in schema_props: - if pname not in params: - params[pname] = part - break - - # 如果有 _source_file,尝试用 Node.js 执行 - source_file = tool_info.get("_source_file", "") - original_name = tool_info.get("original_tool_name", "execute") - - if source_file and os.path.isfile(source_file): - return self._exec_operit_script(source_file, original_name, params) - - # 否则返回参数解析结果(供 AI 理解) - return json.dumps({ - "status": "parsed", - "tool": tool_name, - "original_tool": original_name, - "source": source_file, - "params": params, - "note": "如需执行,请确保设备上有 Node.js 运行时", - }, ensure_ascii=False) - - return handler - - def _exec_operit_script(self, source_file: str, func_name: str, params: Dict) -> str: - """通过 Node.js 执行 Operit 脚本中的指定函数""" - import subprocess - import tempfile - - # 生成一个临时执行器脚本 - runner_code = f""" -const path = require('path'); -const mod = require({json.dumps(source_file)}); -const func = mod[{json.dumps(func_name)}]; -if (!func) {{ - console.log(JSON.stringify({{error: "函数 '{func_name}' 不存在"}})); - process.exit(1); -}} -const params = {json.dumps(params)}; -func(params).then(result => {{ - console.log(JSON.stringify(result, null, 2)); -}}).catch(err => {{ - console.log(JSON.stringify({{error: err.message}})); - process.exit(1); -}}); -""" - try: - with tempfile.NamedTemporaryFile(mode='w', suffix='.js', delete=False, - encoding='utf-8') as f: - f.write(runner_code) - runner_path = f.name - - result = subprocess.run( - ["node", runner_path], - capture_output=True, text=True, timeout=30, - ) - os.unlink(runner_path) - - if result.returncode == 0: - return result.stdout.strip() - else: - return f"执行失败: {result.stderr.strip() or result.stdout.strip()}" - except FileNotFoundError: - return ("错误: 未找到 Node.js 运行时。\n" - "请安装 Node.js 以执行 Operit 脚本,或将脚本移植为 Python 版本。") - except subprocess.TimeoutExpired: - return "错误: 脚本执行超时 (30s)" - except Exception as e: - return f"执行错误: {e}" diff --git a/scripts/12306.ts b/scripts/12306.ts deleted file mode 100644 index e5038f3..0000000 --- a/scripts/12306.ts +++ /dev/null @@ -1,1015 +0,0 @@ -/* METADATA -{ - "name": "12306_ticket", - - "display_name": { - "zh": "12306 拓展", - "en": "12306 Extension" - }, - "description": { "zh": "提供12306火车票信息查询功能,包括余票、中转、经停站等。", "en": "Query China Railway 12306 train ticket information, including availability, transfer routes, and stop stations." }, - "enabledByDefault": true, - "category": "Life", - "tools": [ - { - "name": "get_current_date", - "description": { "zh": "获取当前日期,以上海时区(Asia/Shanghai, UTC+8)为准,返回格式为 'yyyy-MM-dd'。主要用于解析用户提到的相对日期(如“明天”、“下周三”),为其他需要日期的接口提供准确的日期输入。", "en": "Get the current date in the Shanghai timezone (Asia/Shanghai, UTC+8). Returns format 'yyyy-MM-dd'. Mainly used to resolve relative dates (e.g. \"tomorrow\", \"next Wednesday\") and provide correct date input for other APIs." }, - "parameters": [] - }, - { - "name": "get_stations_code_in_city", - "description": { "zh": "通过中文城市名查询该城市 **所有** 火车站的名称及其对应的 `station_code`,结果是一个包含多个车站信息的列表。", "en": "Given a Chinese city name, list **all** train stations in that city and their corresponding `station_code`." }, - "parameters": [ - { "name": "city", "description": { "zh": "中文城市名称,例如:'北京', '上海'", "en": "Chinese city name, e.g. '北京', '上海'." }, "type": "string", "required": true } - ] - }, - { - "name": "get_station_code_of_citys", - "description": { "zh": "通过中文城市名查询代表该城市的 `station_code`。此接口主要用于在用户提供**城市名**作为出发地或到达地时,为接口准备 `station_code` 参数。", "en": "Get the representative `station_code` for a Chinese city name. Use this when the user provides a **city name** as origin/destination and you need a `station_code`." }, - "parameters": [ - { "name": "citys", "description": { "zh": "要查询的城市,比如'北京'。若要查询多个城市,请用|分割,比如'北京|上海'。", "en": "City to query, e.g. '北京'. For multiple cities, separate with |, e.g. '北京|上海'." }, "type": "string", "required": true } - ] - }, - { - "name": "get_station_code_by_names", - "description": { "zh": "通过具体的中文车站名查询其 `station_code` 和车站名。此接口主要用于在用户提供**具体车站名**作为出发地或到达地时,为接口准备 `station_code` 参数。", "en": "Given a specific Chinese station name, return its `station_code` and station name. Use this when the user provides a **specific station name** for origin/destination." }, - "parameters": [ - { "name": "station_names", "description": { "zh": "具体的中文车站名称,例如:'北京南', '上海虹桥'。若要查询多个站点,请用|分割,比如'北京南|上海虹桥'。", "en": "Specific Chinese station names, e.g. '北京南', '上海虹桥'. For multiple stations, separate with |, e.g. '北京南|上海虹桥'." }, "type": "string", "required": true } - ] - }, - { - "name": "get_station_by_telecode", - "description": { "zh": "通过车站的 `station_telecode` 查询车站的详细信息,包括名称、拼音、所属城市等。此接口主要用于在已知 `telecode` 的情况下获取更完整的车站数据,或用于特殊查询及调试目的。一般用户对话流程中较少直接触发。", "en": "Query station details by `station_telecode`, including name, pinyin, city, etc. Mainly for getting more complete station data when `telecode` is known, or for special queries/debugging." }, - "parameters": [ - { "name": "station_telecode", "description": { "zh": "车站的 `station_telecode` (3位字母编码)", "en": "Station `station_telecode` (3-letter code)." }, "type": "string", "required": true } - ] - }, - { - "name": "get_tickets", - "description": { "zh": "查询12306余票信息。", "en": "Query 12306 ticket availability." }, - "parameters": [ - { "name": "date", "description": { "zh": "查询日期,格式为 'yyyy-MM-dd'。如果用户提供的是相对日期(如“明天”),请务必先调用 `get_current_date` 接口获取当前日期,并计算出目标日期。", "en": "Query date in 'yyyy-MM-dd'. If the user gives a relative date (e.g. \"tomorrow\"), call `get_current_date` first and compute the target date." }, "type": "string", "required": true }, - { "name": "from_station", "description": { "zh": "出发地的 `station_code` 。必须是通过 `get_station_code_by_names` 或 `get_station_code_of_citys` 接口查询得到的编码,严禁直接使用中文地名。", "en": "Origin `station_code`. Must be obtained via `get_station_code_by_names` or `get_station_code_of_citys` (do NOT pass Chinese names directly)." }, "type": "string", "required": true }, - { "name": "to_station", "description": { "zh": "到达地的 `station_code` 。必须是通过 `get_station_code_by_names` 或 `get_station_code_of_citys` 接口查询得到的编码,严禁直接使用中文地名。", "en": "Destination `station_code`. Must be obtained via `get_station_code_by_names` or `get_station_code_of_citys` (do NOT pass Chinese names directly)." }, "type": "string", "required": true }, - { "name": "train_filter_flags", "description": { "zh": "车次筛选条件,默认为空,即不筛选。支持多个标志同时筛选。例如用户说“高铁票”,则应使用 'G'。可选标志:[G(高铁/城际),D(动车),Z(直达特快),T(特快),K(快速),O(其他),F(复兴号),S(智能动车组)]", "en": "Train filter flags. Default empty (no filter). Can combine multiple flags. Example: for high-speed rail, use 'G'. Options: [G(High-speed/Intercity),D(EMU),Z(Direct express),T(Express),K(Fast),O(Other),F(Fuxing),S(Smart EMU)]." }, "type": "string", "required": false }, - { "name": "sort_flag", "description": { "zh": "排序方式,默认为空,即不排序。仅支持单一标识。可选标志:[startTime(出发时间从早到晚), arriveTime(抵达时间从早到晚), duration(历时从短到长)]", "en": "Sort mode. Default empty (no sorting). Only one mode is supported. Options: [startTime (earliest departure), arriveTime (earliest arrival), duration (shortest duration)]." }, "type": "string", "required": false }, - { "name": "sort_reverse", "description": { "zh": "是否逆向排序结果,默认为false。仅在设置了sortFlag时生效。", "en": "Reverse sort order (default: false). Only effective when sort_flag is set." }, "type": "boolean", "required": false }, - { "name": "limited_num", "description": { "zh": "返回的余票数量限制,默认为0,即不限制。", "en": "Limit number of returned results (default: 0, no limit)." }, "type": "number", "required": false } - ] - }, - { - "name": "get_interline_tickets", - "description": { "zh": "查询12306中转余票信息。尚且只支持查询前十条。", "en": "Query 12306 transfer (interline) ticket availability. Currently only supports the first 10 results." }, - "parameters": [ - { "name": "date", "description": { "zh": "查询日期,格式为 'yyyy-MM-dd'。如果用户提供的是相对日期(如“明天”),请务必先调用 `get_current_date` 接口获取当前日期,并计算出目标日期。", "en": "Query date in 'yyyy-MM-dd'. If the user gives a relative date, call `get_current_date` first and compute the target date." }, "type": "string", "required": true }, - { "name": "from_station", "description": { "zh": "出发地的 `station_code` 。必须是通过 `get-station_code_by_names` 或 `get_station_code_of_citys` 接口查询得到的编码,严禁直接使用中文地名。", "en": "Origin `station_code`. Must be obtained via station-code lookup APIs (do NOT pass Chinese names directly)." }, "type": "string", "required": true }, - { "name": "to_station", "description": { "zh": "到达地的 `station_code` 。必须是通过 `get_station_code_by_names` 或 `get_station_code_of_citys` 接口查询得到的编码,严禁直接使用中文地名。", "en": "Destination `station_code`. Must be obtained via station-code lookup APIs (do NOT pass Chinese names directly)." }, "type": "string", "required": true }, - { "name": "middle_station", "description": { "zh": "中转地的 `station_code` ,可选。必须是通过 `get-station-code-by-names` 或 `get-station_code_of_citys` 接口查询得到的编码,严禁直接使用中文地名。", "en": "Optional transfer station `station_code`. Must be obtained via station-code lookup APIs (do NOT pass Chinese names directly)." }, "type": "string", "required": false }, - { "name": "show_wz", "description": { "zh": "是否显示无座车,默认不显示无座车。", "en": "Whether to include no-seat (无座) tickets (default: false)." }, "type": "boolean", "required": false }, - { "name": "train_filter_flags", "description": { "zh": "车次筛选条件,默认为空。从以下标志中选取多个条件组合[G(高铁/城际),D(动车),Z(直达特快),T(特快),K(快速),O(其他),F(复兴号),S(智能动车组)]", "en": "Train filter flags. Default empty. Combine multiple flags from: [G(High-speed/Intercity),D(EMU),Z(Direct express),T(Express),K(Fast),O(Other),F(Fuxing),S(Smart EMU)]." }, "type": "string", "required": false }, - { "name": "sort_flag", "description": { "zh": "排序方式,默认为空,即不排序。仅支持单一标识。可选标志:[startTime(出发时间从早到晚), arriveTime(抵达时间从早到晚), duration(历时从短到长)]", "en": "Sort mode. Default empty. Options: startTime / arriveTime / duration." }, "type": "string", "required": false }, - { "name": "sort_reverse", "description": { "zh": "是否逆向排序结果,默认为false。仅在设置了sortFlag时生效。", "en": "Reverse sort order (default: false). Only effective when sort_flag is set." }, "type": "boolean", "required": false }, - { "name": "limited_num", "description": { "zh": "返回的中转余票数量限制,默认为10。", "en": "Limit number of returned results (default: 10)." }, "type": "number", "required": false } - ] - }, - { - "name": "get_train_route_stations", - "description": { "zh": "查询特定列车车次在指定区间内的途径车站、到站时间、出发时间及停留时间等详细经停信息。当用户询问某趟具体列车的经停站时使用此接口。", "en": "Query detailed stop information for a specific train within a segment, including stations, arrival/departure times, and stop duration. Use when the user asks for stops of a specific train." }, - "parameters": [ - { "name": "train_no", "description": { "zh": "要查询的实际车次编号 `train_no`,例如 '240000G10336',而非'G1033'。此编号通常可以从 `get_tickets` 的查询结果中获取,或者由用户直接提供。", "en": "Actual train number `train_no`, e.g. '240000G10336' (not 'G1033'). Usually obtained from `get_tickets` results or provided by the user." }, "type": "string", "required": true }, - { "name": "from_station_telecode", "description": { "zh": "该列车行程的**出发站**的 `station_telecode` (3位字母编码`)。通常来自 `get_tickets` 结果中的 `telecode` 字段,或者通过 `get_station_code_by_names` 得到。", "en": "`station_telecode` (3-letter code) of the **origin station**. Usually from `telecode` fields in `get_tickets`, or obtained via station code lookup." }, "type": "string", "required": true }, - { "name": "to_station_telecode", "description": { "zh": "该列车行程的**到达站**的 `station_telecode` (3位字母编码)。通常来自 `get_tickets` 结果中的 `telecode` 字段,或者通过 `get-station_code_by_names` 得到。", "en": "`station_telecode` (3-letter code) of the **destination station**. Usually from `telecode` fields in `get_tickets`, or obtained via station code lookup." }, "type": "string", "required": true }, - { "name": "depart_date", "description": { "zh": "列车从 `from_station_telecode` 指定的车站出发的日期 (格式: yyyy-MM-dd)。如果用户提供的是相对日期,请务必先调用 `get_current_date` 解析。", "en": "Departure date from the origin station (format: yyyy-MM-dd). If the user provides a relative date, resolve it via `get_current_date`." }, "type": "string", "required": true } - ] - } - ] -}*/ - -// #region 类型定义 -type TicketData = { - secret_Sstr: string; - button_text_info: string; - train_no: string; - station_train_code: string; - start_station_telecode: string; - end_station_telecode: string; - from_station_telecode: string; - to_station_telecode: string; - start_time: string; - arrive_time: string; - lishi: string; - canWebBuy: string; - yp_info: string; - start_train_date: string; - train_seat_feature: string; - location_code: string; - from_station_no: string; - to_station_no: string; - is_support_card: string; - controlled_train_flag: string; - gg_num: string; - gr_num: string; - qt_num: string; - rw_num: string; - rz_num: string; - tz_num: string; - wz_num: string; - yb_num: string; - yw_num: string; - yz_num: string; - ze_num: string; - zy_num: string; - swz_num: string; - srrb_num: string; - yp_ex: string; - seat_types: string; - exchange_train_flag: string; - houbu_train_flag: string; - houbu_seat_limit: string; - yp_info_new: string; - '40': string; - '41': string; - '42': string; - '43': string; - '44': string; - '45': string; - dw_flag: string; - '47': string; - stopcheckTime: string; - country_flag: string; - local_arrive_time: string; - local_start_time: string; - '52': string; - bed_level_info: string; - seat_discount_info: string; - sale_time: string; - '56': string; -}; - -const TicketDataKeys: (keyof TicketData)[] = [ - 'secret_Sstr', 'button_text_info', 'train_no', 'station_train_code', 'start_station_telecode', - 'end_station_telecode', 'from_station_telecode', 'to_station_telecode', 'start_time', 'arrive_time', - 'lishi', 'canWebBuy', 'yp_info', 'start_train_date', 'train_seat_feature', - 'location_code', 'from_station_no', 'to_station_no', 'is_support_card', 'controlled_train_flag', - 'gg_num', 'gr_num', 'qt_num', 'rw_num', 'rz_num', - 'tz_num', 'wz_num', 'yb_num', 'yw_num', 'yz_num', - 'ze_num', 'zy_num', 'swz_num', 'srrb_num', 'yp_ex', - 'seat_types', 'exchange_train_flag', 'houbu_train_flag', 'houbu_seat_limit', 'yp_info_new', - '40', '41', '42', '43', '44', - '45', 'dw_flag', '47', 'stopcheckTime', 'country_flag', - 'local_arrive_time', 'local_start_time', '52', 'bed_level_info', 'seat_discount_info', - 'sale_time', '56', -]; - -type TicketInfo = { - train_no: string; - start_train_code: string; - start_date: string; - start_time: string; - arrive_date: string; - arrive_time: string; - lishi: string; - from_station: string; - to_station: string; - from_station_telecode: string; - to_station_telecode: string; - prices: Price[]; - dw_flag: string[]; -}; - -type StationData = { - station_id: string; - station_name: string; - station_code: string; - station_pinyin: string; - station_short: string; - station_index: string; - code: string; - city: string; - r1: string; - r2: string; -}; - -const StationDataKeys: (keyof StationData)[] = [ - 'station_id', 'station_name', 'station_code', 'station_pinyin', 'station_short', - 'station_index', 'code', 'city', 'r1', 'r2', -]; - -interface Price { - seat_name: string; - short: string; - seat_type_code: string; - num: string; - price: number; - discount: number | undefined; -} - -type RouteStationData = { - arrive_time: string; - station_name: string; - isChina: string; - start_time: string; - stopover_time: string; - station_no: string; - country_code: string; - country_name: string; - isEnabled: boolean; - train_class_name?: string; - service_type?: string; - end_station_name?: string; - start_station_name?: string; - station_train_code?: string; -}; - -type RouteStationInfo = { - arrive_time: string; - station_name: string; - stopover_time: string; - station_no: number; -}; - -type InterlineData = { - all_lishi: string; - all_lishi_minutes: number; - arrive_date: string; - arrive_time: string; - end_station_code: string; - end_station_name: string; - first_train_no: string; - from_station_code: string; - from_station_name: string; - fullList: InterlineTicketData[]; - isHeatTrain: string; - isOutStation: string; - lCWaitTime: string; - lishi_flag: string; - middle_date: string; - middle_station_code: string; - middle_station_name: string; - same_station: string; - same_train: string; - score: number; - score_str: string; - scretstr: string; - second_train_no: string; - start_time: string; - train_count: number; - train_date: string; // 出发时间 - use_time: string; - wait_time: string; - wait_time_minutes: number; -}; - -type InterlineInfo = { - lishi: string; - start_time: string; - start_date: string; - middle_date: string; - arrive_date: string; - arrive_time: string; - from_station_code: string; - from_station_name: string; - middle_station_code: string; - middle_station_name: string; - end_station_code: string; - end_station_name: string; - start_train_code: string; // 用于过滤 - first_train_no: string; - second_train_no: string; - train_count: number; - ticketList: TicketInfo[]; - same_station: boolean; - same_train: boolean; - wait_time: string; -}; - -type InterlineTicketData = { - arrive_time: string; - bed_level_info: string; - controlled_train_flag: string; - country_flag: string; - day_difference: string; - dw_flag: string; - end_station_name: string; - end_station_telecode: string; - from_station_name: string; - from_station_no: string; - from_station_telecode: string; - gg_num: string; - gr_num: string; - is_support_card: string; - lishi: string; - local_arrive_time: string; - local_start_time: string; - qt_num: string; - rw_num: string; - rz_num: string; - seat_discount_info: string; - seat_types: string; - srrb_num: string; - start_station_name: string; - start_station_telecode: string; - start_time: string; - start_train_date: string; - station_train_code: string; - swz_num: string; - to_station_name: string; - to_station_no: string; - to_station_telecode: string; - train_no: string; - train_seat_feature: string; - trms_train_flag: string; - tz_num: string; - wz_num: string; - yb_num: string; - yp_info: string; - yw_num: string; - yz_num: string; - ze_num: string; - zy_num: string; -}; - -// #endregion - -const ticket12306 = (function () { - - const API_BASE = 'https://kyfw.12306.cn'; - const WEB_URL = 'https://www.12306.cn/index/'; - const LCQUERY_INIT_URL = 'https://kyfw.12306.cn/otn/lcQuery/init'; - - let LCQUERY_PATH: string | undefined = undefined; - const MISSING_STATIONS: StationData[] = [ - { station_id: '@cdd', station_name: '成 都东', station_code: 'WEI', station_pinyin: 'chengdudong', station_short: 'cdd', station_index: '', code: '1707', city: '成都', r1: '', r2: '' }, - ]; - - let STATIONS: Record | undefined = undefined; - let CITY_STATIONS: Record | undefined = undefined; - let CITY_CODES: Record | undefined = undefined; - let NAME_STATIONS: Record | undefined = undefined; - - const SEAT_SHORT_TYPES = { swz: '商务座', tz: '特等座', zy: '一等座', ze: '二等座', gr: '高软卧', srrb: '动卧', rw: '软卧', yw: '硬卧', rz: '软座', yz: '硬座', wz: '无座', qt: '其他', gg: '', yb: '' }; - const SEAT_TYPES = { - '9': { name: '商务座', short: 'swz' }, P: { name: '特等座', short: 'tz' }, M: { name: '一等座', short: 'zy' }, D: { name: '优选一等座', short: 'zy' }, O: { name: '二等座', short: 'ze' }, S: { name: '二等包座', short: 'ze' }, '6': { name: '高级软卧', short: 'gr' }, A: { name: '高级动卧', short: 'gr' }, '4': { name: '软卧', short: 'rw' }, I: { name: '一等卧', short: 'rw' }, F: { name: '动卧', short: 'rw' }, '3': { name: '硬卧', short: 'yw' }, J: { name: '二等卧', short: 'yw' }, '2': { name: '软座', short: 'rz' }, '1': { name: '硬座', short: 'yz' }, W: { name: '无座', short: 'wz' }, WZ: { name: '无座', short: 'wz' }, H: { name: '其他', short: 'qt' }, - }; - const DW_FLAGS = ['智能动车组', '复兴号', '静音车厢', '温馨动卧', '动感号', '支持选铺', '老年优惠']; - - const client = OkHttp.newClient(); - let initPromise: Promise | undefined = undefined; - - // #region 辅助函数 - function formatDate(date: Date): string { - const year = date.getUTCFullYear(); - const month = (date.getUTCMonth() + 1).toString().padStart(2, '0'); - const day = date.getUTCDate().toString().padStart(2, '0'); - return `${year}-${month}-${day}`; - } - - function parseDate(dateStr: string): Date { // yyyyMMdd - const year = parseInt(dateStr.substring(0, 4), 10); - const month = parseInt(dateStr.substring(4, 6), 10) - 1; - const day = parseInt(dateStr.substring(6, 8), 10); - return new Date(Date.UTC(year, month, day)); - } - - function getCurrentShanghaiDate(): Date { - const now = new Date(); - return new Date(now.getTime() + 8 * 60 * 60 * 1000); - } - - function checkDate(dateStr: string): boolean { // yyyy-MM-dd - const todayInShanghai = getCurrentShanghaiDate(); - todayInShanghai.setUTCHours(0, 0, 0, 0); - - const parts = dateStr.split('-').map(p => parseInt(p, 10)); - const inputDate = new Date(Date.UTC(parts[0], parts[1] - 1, parts[2])); - - return inputDate.getTime() >= todayInShanghai.getTime(); - } - - function parseCookies(cookies: string[]): Record { - const cookieRecord: Record = {}; - if (!cookies) return cookieRecord; - cookies.forEach((cookie) => { - const keyValuePart = cookie.split(';')[0]; - const [key, value] = keyValuePart.split('='); - if (key && value) { - cookieRecord[key.trim()] = value.trim(); - } - }); - return cookieRecord; - } - - function formatCookies(cookies: Record): string { - return Object.entries(cookies).map(([key, value]) => `${key}=${value}`).join('; '); - } - - async function getCookie(): Promise | undefined> { - const url = `${API_BASE}/otn/leftTicket/init`; - try { - const response = await client.newRequest().url(url).build().execute(); - const cookieHeader = response.headers && (response.headers['set-cookie'] || response.headers['Set-Cookie']); - if (cookieHeader) { - const parsed = parseCookies(Array.isArray(cookieHeader) ? cookieHeader : [cookieHeader]); - if (Object.keys(parsed).length > 0) { - return parsed; - } - } - // If no cookies are in the header, assume the client is stateful. - // Return an empty object to signal success and rely on the client's cookie jar. - return {}; - } catch (error) { - console.error('Error getting 12306 cookie:', error); - return undefined; - } - } - - async function make12306Request(url: string, params: Record = {}, headers: Record = {}): Promise { - const queryString = Object.entries(params).map(([key, val]) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`).join('&'); - const fullUrl = queryString ? `${url}?${queryString}` : url; - try { - const finalHeaders = { ...headers }; - // If the cookie string is empty, remove it to let the client use its cookie jar. - if (finalHeaders['Cookie'] === '') { - delete finalHeaders['Cookie']; - } - const request = client.newRequest().url(fullUrl).method('GET').headers(finalHeaders); - const response = await request.build().execute(); - if (!response.isSuccessful()) { - throw new Error(`HTTP error! status: ${response.statusCode}`); - } - return JSON.parse(response.content); - } catch (error) { - console.error(`Error making 12306 request to ${fullUrl}:`, error); - return undefined; - } - } - - async function make12306RequestHtml(url: string): Promise { - try { - const request = client.newRequest().url(url).method('GET'); - const response = await request.build().execute(); - if (!response.isSuccessful()) { - throw new Error(`HTTP error! status: ${response.statusCode}`); - } - return response.content; - } catch (error) { - console.error(`Error fetching HTML from ${url}:`, error); - return undefined; - } - } - - function parseTicketsData(rawData: string[]): TicketData[] { - const result: TicketData[] = []; - for (const item of rawData) { - const values = item.split('|'); - const entry: Partial = {}; - TicketDataKeys.forEach((key, index) => { - entry[key] = values[index]; - }); - result.push(entry as TicketData); - } - return result; - } - - function extractPrices(yp_info: string, seat_discount_info: string, ticketData: TicketData | InterlineTicketData): Price[] { - const PRICE_STR_LENGTH = 10; - const DISCOUNT_STR_LENGTH = 5; - const prices: Price[] = []; - const discounts: { [key: string]: number } = {}; - for (let i = 0; i < seat_discount_info.length / DISCOUNT_STR_LENGTH; i++) { - const discount_str = seat_discount_info.slice(i * DISCOUNT_STR_LENGTH, (i + 1) * DISCOUNT_STR_LENGTH); - discounts[discount_str[0]] = parseInt(discount_str.slice(1), 10); - } - - for (let i = 0; i < yp_info.length / PRICE_STR_LENGTH; i++) { - const price_str = yp_info.slice(i * PRICE_STR_LENGTH, (i + 1) * PRICE_STR_LENGTH); - var seat_type_code; - if (parseInt(price_str.slice(6, 10), 10) >= 3000) { - seat_type_code = 'W'; // 为无座 - } else if (!Object.keys(SEAT_TYPES).includes(price_str[0])) { - seat_type_code = 'H'; // 其他坐席 - } else { - seat_type_code = price_str[0]; - } - const seat_type = SEAT_TYPES[seat_type_code as keyof typeof SEAT_TYPES]; - const price = parseInt(price_str.slice(1, 6), 10) / 10; - const discount = seat_type_code in discounts ? discounts[seat_type_code] : undefined; - prices.push({ - seat_name: seat_type.name, - short: seat_type.short, - seat_type_code, - num: ticketData[`${seat_type.short}_num` as keyof (TicketData | InterlineTicketData)], - price, - discount, - }); - } - return prices; - } - - function extractDWFlags(dw_flag_str: string): string[] { - const dwFlagList = dw_flag_str.split('#'); - let result: string[] = []; - if ('5' == dwFlagList[0]) { result.push(DW_FLAGS[0]); } - if (dwFlagList.length > 1 && '1' == dwFlagList[1]) { result.push(DW_FLAGS[1]); } - if (dwFlagList.length > 2) { - if ('Q' == dwFlagList[2].substring(0, 1)) { result.push(DW_FLAGS[2]); } - else if ('R' == dwFlagList[2].substring(0, 1)) { result.push(DW_FLAGS[3]); } - } - if (dwFlagList.length > 5 && 'D' == dwFlagList[5]) { result.push(DW_FLAGS[4]); } - if (dwFlagList.length > 6 && 'z' != dwFlagList[6]) { result.push(DW_FLAGS[5]); } - if (dwFlagList.length > 7 && 'z' != dwFlagList[7]) { result.push(DW_FLAGS[6]); } - return result; - } - - function parseTicketsInfo(ticketsData: TicketData[], map: Record): TicketInfo[] { - const result: TicketInfo[] = []; - for (const ticket of ticketsData) { - const prices = extractPrices(ticket.yp_info_new, ticket.seat_discount_info, ticket); - const dw_flag = extractDWFlags(ticket.dw_flag); - const startDate = parseDate(ticket.start_train_date); - const [startHours, startMinutes] = ticket.start_time.split(':').map(Number); - const [durationHours, durationMinutes] = ticket.lishi.split(':').map(Number); - - const arriveDate = new Date(startDate); - arriveDate.setUTCHours(arriveDate.getUTCHours() + startHours + durationHours, arriveDate.getUTCMinutes() + startMinutes + durationMinutes); - - result.push({ - train_no: ticket.train_no, - start_date: formatDate(startDate), - arrive_date: formatDate(arriveDate), - start_train_code: ticket.station_train_code, - start_time: ticket.start_time, - arrive_time: ticket.arrive_time, - lishi: ticket.lishi, - from_station: map[ticket.from_station_telecode], - to_station: map[ticket.to_station_telecode], - from_station_telecode: ticket.from_station_telecode, - to_station_telecode: ticket.to_station_telecode, - prices: prices, - dw_flag: dw_flag, - }); - } - return result; - } - - function formatTicketStatus(num: string): string { - if (num.match(/^\d+$/)) { - const count = parseInt(num); - return count === 0 ? '无票' : `剩余${count}张票`; - } - switch (num) { - case '有': case '充足': return '有票'; - case '无': case '--': case '': return '无票'; - case '候补': return '无票需候补'; - default: return `${num}票`; - } - } - - function formatTicketsInfo(ticketsInfo: TicketInfo[]): string { - if (ticketsInfo.length === 0) return '没有查询到相关车次信息'; - let result = '车次 | 出发站 -> 到达站 | 出发时间 -> 到达时间 | 历时\n'; - ticketsInfo.forEach((ticketInfo) => { - let infoStr = `${ticketInfo.start_train_code}(实际车次train_no: ${ticketInfo.train_no}) ${ticketInfo.from_station}(telecode: ${ticketInfo.from_station_telecode}) -> ${ticketInfo.to_station}(telecode: ${ticketInfo.to_station_telecode}) ${ticketInfo.start_time} -> ${ticketInfo.arrive_time} 历时:${ticketInfo.lishi}`; - ticketInfo.prices.forEach((price) => { - infoStr += `\n- ${price.seat_name}: ${formatTicketStatus(price.num)} ${price.price}元`; - }); - result += `${infoStr}\n`; - }); - return result; - } - - const TRAIN_FILTERS = { - G: (t: TicketInfo | InterlineInfo) => t.start_train_code.startsWith('G') || t.start_train_code.startsWith('C'), - D: (t: TicketInfo | InterlineInfo) => t.start_train_code.startsWith('D'), - Z: (t: TicketInfo | InterlineInfo) => t.start_train_code.startsWith('Z'), - T: (t: TicketInfo | InterlineInfo) => t.start_train_code.startsWith('T'), - K: (t: TicketInfo | InterlineInfo) => t.start_train_code.startsWith('K'), - O: (t: TicketInfo | InterlineInfo) => !/^[GDZTK]/.test(t.start_train_code), - F: (t: TicketInfo | InterlineInfo) => 'dw_flag' in t ? t.dw_flag.includes('复兴号') : t.ticketList[0].dw_flag.includes('复兴号'), - S: (t: TicketInfo | InterlineInfo) => 'dw_flag' in t ? t.dw_flag.includes('智能动车组') : t.ticketList[0].dw_flag.includes('智能动车组'), - }; - - const TIME_COMPARETOR = { - startTime: (a: TicketInfo | InterlineInfo, b: TicketInfo | InterlineInfo) => new Date(`${a.start_date} ${a.start_time}`).getTime() - new Date(`${b.start_date} ${b.start_time}`).getTime(), - arriveTime: (a: TicketInfo | InterlineInfo, b: TicketInfo | InterlineInfo) => new Date(`${a.arrive_date} ${a.arrive_time}`).getTime() - new Date(`${b.arrive_date} ${b.arrive_time}`).getTime(), - duration: (a: TicketInfo | InterlineInfo, b: TicketInfo | InterlineInfo) => { - const [hA, mA] = a.lishi.split(':').map(Number); - const [hB, mB] = b.lishi.split(':').map(Number); - return (hA * 60 + mA) - (hB * 60 + mB); - }, - }; - - function filterTicketsInfo(ticketsInfo: T[], trainFilterFlags: string, sortFlag: string = '', sortReverse: boolean = false, limitedNum: number = 0): T[] { - let result = trainFilterFlags ? ticketsInfo.filter(t => [...trainFilterFlags].some(flag => TRAIN_FILTERS[flag as keyof typeof TRAIN_FILTERS](t))) : ticketsInfo; - if (Object.keys(TIME_COMPARETOR).includes(sortFlag)) { - result.sort(TIME_COMPARETOR[sortFlag as keyof typeof TIME_COMPARETOR]); - if (sortReverse) result.reverse(); - } - return limitedNum > 0 ? result.slice(0, limitedNum) : result; - } - - function parseRouteStationsInfo(routeStationsData: RouteStationData[]): RouteStationInfo[] { - return routeStationsData.map((routeStationData, index) => ({ - arrive_time: index === 0 ? routeStationData.start_time : routeStationData.arrive_time, - station_name: routeStationData.station_name, - stopover_time: routeStationData.stopover_time, - station_no: parseInt(routeStationData.station_no), - })); - } - - function parseInterlinesTicketInfo(interlineTicketsData: InterlineTicketData[]): TicketInfo[] { - return interlineTicketsData.map(ticket => { - const prices = extractPrices(ticket.yp_info, ticket.seat_discount_info, ticket); - const startDate = parseDate(ticket.start_train_date); - const [startHours, startMinutes] = ticket.start_time.split(':').map(Number); - const [durationHours, durationMinutes] = ticket.lishi.split(':').map(Number); - - const arriveDate = new Date(startDate); - arriveDate.setUTCHours(arriveDate.getUTCHours() + startHours + durationHours, arriveDate.getUTCMinutes() + startMinutes + durationMinutes); - - return { - train_no: ticket.train_no, - start_train_code: ticket.station_train_code, - start_date: formatDate(startDate), - arrive_date: formatDate(arriveDate), - start_time: ticket.start_time, - arrive_time: ticket.arrive_time, - lishi: ticket.lishi, - from_station: ticket.from_station_name, - to_station: ticket.to_station_name, - from_station_telecode: ticket.from_station_telecode, - to_station_telecode: ticket.to_station_telecode, - prices: prices, - dw_flag: extractDWFlags(ticket.dw_flag), - }; - }); - } - - function extractLishi(all_lishi: string): string { - const match = all_lishi.match(/(?:(\d+)小时)?(\d+)分钟/); - if (!match) return '00:00'; - const hours = (match[1] || '0').padStart(2, '0'); - const minutes = (match[2] || '0').padStart(2, '0'); - return `${hours}:${minutes}`; - } - - function parseInterlinesInfo(interlineData: InterlineData[]): InterlineInfo[] { - return interlineData.map(ticket => ({ - lishi: extractLishi(ticket.all_lishi), - start_time: ticket.start_time, - start_date: ticket.train_date, - middle_date: ticket.middle_date, - arrive_date: ticket.arrive_date, - arrive_time: ticket.arrive_time, - from_station_code: ticket.from_station_code, - from_station_name: ticket.from_station_name, - middle_station_code: ticket.middle_station_code, - middle_station_name: ticket.middle_station_name, - end_station_code: ticket.end_station_code, - end_station_name: ticket.end_station_name, - start_train_code: ticket.fullList[0].station_train_code, - first_train_no: ticket.first_train_no, - second_train_no: ticket.second_train_no, - train_count: ticket.train_count, - ticketList: parseInterlinesTicketInfo(ticket.fullList), - same_station: ticket.same_station == '0', - same_train: ticket.same_train == 'Y', - wait_time: ticket.wait_time, - })); - } - - function formatInterlinesInfo(interlinesInfo: InterlineInfo[]): string { - if (interlinesInfo.length === 0) return '没有查询到相关的中转车次信息'; - let result = '出发时间 -> 到达时间 | 出发车站 -> 中转车站 -> 到达车站 | 换乘标志 | 换乘等待时间 | 总历时\n\n'; - interlinesInfo.forEach((info) => { - result += `${info.start_date} ${info.start_time} -> ${info.arrive_date} ${info.arrive_time} | `; - result += `${info.from_station_name} -> ${info.middle_station_name} -> ${info.end_station_name} | `; - result += `${info.same_train ? '同车换乘' : info.same_station ? '同站换乘' : '换站换乘'} | ${info.wait_time} | ${info.lishi}\n\n`; - result += '\t' + formatTicketsInfo(info.ticketList).replace(/\n/g, '\n\t') + '\n'; - }); - return result; - } - - function parseStationsData(rawData: string): Record { - const result: Record = {}; - const dataArray = rawData.split('|'); - for (let i = 0; i < dataArray.length; i += 10) { - const group = dataArray.slice(i, i + 10); - if (group.length < 10) continue; - let station: Partial = {}; - StationDataKeys.forEach((key, index) => { - station[key] = group[index]; - }); - if (station.station_code) { - result[station.station_code!] = station as StationData; - } - } - return result; - } - - async function getStationsInternal(): Promise> { - const stationNameJSUrl = "https://kyfw.12306.cn/otn/resources/js/framework/station_name.js"; - const stationNameJS = await make12306RequestHtml(stationNameJSUrl); - if (!stationNameJS) throw new Error('Error: get station name js file content failed.'); - - const rawDataMatch = stationNameJS.match(/var station_names\s*=\s*'(.*?)';/); - if (!rawDataMatch) throw new Error('Error: could not find station data in JS file.'); - - const rawData = rawDataMatch[1]; - const stationsData = parseStationsData(rawData); - - for (const station of MISSING_STATIONS) { - if (!stationsData[station.station_code]) { - stationsData[station.station_code] = station; - } - } - return stationsData; - } - - async function getLCQueryPath(): Promise { - const html = await make12306RequestHtml(LCQUERY_INIT_URL); - if (html == undefined) throw new Error('Error: get 12306 web page for LCQuery path failed.'); - const match = html.match(/var lc_search_url = '(.+?)'/); - if (match == undefined) throw new Error('Error: get LCQuery path failed.'); - return match[1]; - } - - async function init() { - if (initPromise) return initPromise; - initPromise = (async () => { - if (STATIONS) return; - try { - STATIONS = await getStationsInternal(); - LCQUERY_PATH = await getLCQueryPath(); - - CITY_STATIONS = {}; - for (const station of Object.values(STATIONS)) { - const city = station.city; - if (!CITY_STATIONS[city]) CITY_STATIONS[city] = []; - CITY_STATIONS[city].push({ station_code: station.station_code, station_name: station.station_name }); - } - - CITY_CODES = {}; - for (const [city, stations] of Object.entries(CITY_STATIONS)) { - for (const station of stations) { - if (station.station_name == city) { - CITY_CODES[city] = station; - break; - } - } - } - - NAME_STATIONS = {}; - for (const station of Object.values(STATIONS)) { - NAME_STATIONS[station.station_name] = { station_code: station.station_code, station_name: station.station_name }; - } - } catch (e) { - initPromise = undefined; // Reset promise on failure to allow retry - throw e; - } - })(); - return initPromise; - } - // #endregion - - // #region 工具函数实现 - async function get_current_date(params: {}) { - const now = getCurrentShanghaiDate(); - return formatDate(now); - } - - async function get_stations_code_in_city(params: { city: string }) { - await init(); - if (!(params.city in CITY_STATIONS!)) { - throw new Error('City not found.'); - } - return CITY_STATIONS![params.city]; - } - - async function get_station_code_of_citys(params: { citys: string }) { - await init(); - let result: Record = {}; - for (const city of params.citys.split('|')) { - if (!(city in CITY_CODES!)) { - result[city] = { error: '未检索到城市。' }; - } else { - result[city] = CITY_CODES![city]; - } - } - return result; - } - - async function get_station_code_by_names(params: { station_names: string }) { - await init(); - let result: Record = {}; - for (let stationName of params.station_names.split('|')) { - stationName = stationName.endsWith('站') ? stationName.slice(0, -1) : stationName; - if (!(stationName in NAME_STATIONS!)) { - result[stationName] = { error: '未检索到车站。' }; - } else { - result[stationName] = NAME_STATIONS![stationName]; - } - } - return result; - } - - async function get_station_by_telecode(params: { station_telecode: string }) { - await init(); - if (!STATIONS![params.station_telecode]) { - throw new Error('Station not found.'); - } - return STATIONS![params.station_telecode]; - } - - async function get_tickets(params: { date: string, from_station: string, to_station: string, train_filter_flags?: string, sort_flag?: string, sort_reverse?: boolean, limited_num?: number }) { - await init(); - if (!checkDate(params.date)) throw new Error('The date cannot be earlier than today.'); - if (!STATIONS![params.from_station] || !STATIONS![params.to_station]) throw new Error('Station not found.'); - - const queryParams = { - 'leftTicketDTO.train_date': params.date, - 'leftTicketDTO.from_station': params.from_station, - 'leftTicketDTO.to_station': params.to_station, - 'purpose_codes': 'ADULT', - }; - const queryUrl = `${API_BASE}/otn/leftTicket/query`; - const cookies = await getCookie(); - if (!cookies) throw new Error('Get cookie failed. Check your network.'); - - const response = await make12306Request(queryUrl, queryParams, { Cookie: formatCookies(cookies) }); - if (!response || !response.data || !response.data.result) throw new Error('Get tickets data failed.'); - - const ticketsData = parseTicketsData(response.data.result); - const ticketsInfo = parseTicketsInfo(ticketsData, response.data.map); - const filteredTicketsInfo = filterTicketsInfo(ticketsInfo, params.train_filter_flags || '', params.sort_flag, params.sort_reverse, params.limited_num); - - return formatTicketsInfo(filteredTicketsInfo); - } - - async function get_interline_tickets(params: { date: string, from_station: string, to_station: string, middle_station?: string, show_wz?: boolean, train_filter_flags?: string, sort_flag?: string, sort_reverse?: boolean, limited_num?: number }) { - await init(); - if (!checkDate(params.date)) throw new Error('The date cannot be earlier than today.'); - if (!STATIONS![params.from_station] || !STATIONS![params.to_station]) throw new Error('Station not found.'); - - const cookies = await getCookie(); - if (!cookies) throw new Error('Get cookie failed. Check your network.'); - - const limited_num = params.limited_num || 10; - let interlineData: InterlineData[] = []; - const queryParams = { - 'train_date': params.date, - 'from_station_telecode': params.from_station, - 'to_station_telecode': params.to_station, - 'middle_station': params.middle_station || '', - 'result_index': '0', - 'can_query': 'Y', - 'isShowWZ': params.show_wz ? 'Y' : 'N', - 'purpose_codes': '00', - 'channel': 'E', - }; - - while (interlineData.length < limited_num) { - const response = await make12306Request(`${API_BASE}${LCQUERY_PATH}`, queryParams, { Cookie: formatCookies(cookies) }); - if (!response) throw new Error('Request interline tickets data failed.'); - if (typeof response.data === 'string') return `很抱歉,未查到相关的列车余票。(${response.errorMsg})`; - - interlineData.push(...response.data.middleList); - if (response.data.can_query === 'N' || !response.data.middleList || response.data.middleList.length === 0) break; - queryParams.result_index = response.data.result_index.toString(); - } - - const interlineTicketsInfo = parseInterlinesInfo(interlineData); - const filtered = filterTicketsInfo(interlineTicketsInfo, params.train_filter_flags || '', params.sort_flag, params.sort_reverse, limited_num); - - return formatInterlinesInfo(filtered); - } - - async function get_train_route_stations(params: { train_no: string, from_station_telecode: string, to_station_telecode: string, depart_date: string }) { - await init(); - const queryParams = { - 'train_no': params.train_no, - 'from_station_telecode': params.from_station_telecode, - 'to_station_telecode': params.to_station_telecode, - 'depart_date': params.depart_date, - }; - const queryUrl = `${API_BASE}/otn/czxx/queryByTrainNo`; - const cookies = await getCookie(); - if (!cookies) throw new Error('Get cookie failed.'); - - const response = await make12306Request(queryUrl, queryParams, { Cookie: formatCookies(cookies) }); - if (!response || !response.data || !response.data.data) throw new Error('Get train route stations failed.'); - - const routeStationsInfo = parseRouteStationsInfo(response.data.data); - if (routeStationsInfo.length === 0) return '未查询到相关车次信息。'; - - return routeStationsInfo; - } - // #endregion - - async function wrap(func: (params: any) => Promise, params: any, successMessage: string, failMessage: string) { - try { - const result = await func(params); - complete({ success: true, message: successMessage, data: result }); - } catch (error: any) { - console.error(`Function ${func.name} failed! Error: ${error.message}`); - complete({ success: false, message: `${failMessage}: ${error.message}`, error_stack: error.stack }); - } - } - - async function main() { - console.log("--- 开始测试 12306 工具包 ---"); - - try { - await init(); - - console.log("\n[1/8] 测试 get_current_date..."); - const dateResult = await get_current_date({}); - console.log("测试结果:", JSON.stringify(dateResult, undefined, 2)); - const testDate = dateResult as string; - - console.log("\n[2/8] 测试 get_stations_code_in_city (北京)..."); - const cityStations = await get_stations_code_in_city({ city: '北京' }); - console.log("测试结果:", JSON.stringify(cityStations, undefined, 2)); - - console.log("\n[3/8] 测试 get_station_code_of_citys (北京|上海)..."); - const cityCodesResult = await get_station_code_of_citys({ citys: '北京|上海' }); - console.log("测试结果:", JSON.stringify(cityCodesResult, undefined, 2)); - const beijingCode = (cityCodesResult as any)['北京'].station_code; - const shanghaiCode = (cityCodesResult as any)['上海'].station_code; - - console.log("\n[4/8] 测试 get_station_code_by_names (北京南|上海虹桥)..."); - const stationCodesResult = await get_station_code_by_names({ station_names: '北京南|上海虹桥' }); - console.log("测试结果:", JSON.stringify(stationCodesResult, undefined, 2)); - const beijingnanCode = (stationCodesResult as any)['北京南'].station_code; - const shanghaihongqiaoCode = (stationCodesResult as any)['上海虹桥'].station_code; - - console.log("\n[5/8] 测试 get_station_by_telecode (VNP)..."); - const stationInfo = await get_station_by_telecode({ station_telecode: 'VNP' }); // VNP is 北京 - console.log("测试结果:", JSON.stringify(stationInfo, undefined, 2)); - - console.log(`\n[6/8] 测试 get_tickets (${testDate}, from: 北京南, to: 上海虹桥)...`); - const tickets = await get_tickets({ - date: testDate, - from_station: beijingnanCode, - to_station: shanghaihongqiaoCode, - train_filter_flags: 'G' - }); - console.log("测试结果 (部分):", (tickets as string).substring(0, 400) + "..."); - - console.log(`\n[7/8] 测试 get_interline_tickets (${testDate}, from: 北京, to: 上海)...`); - const interlineTickets = await get_interline_tickets({ - date: testDate, - from_station: beijingCode, - to_station: shanghaiCode, - limited_num: 2 - }); - console.log("测试结果 (部分):", (interlineTickets as string).substring(0, 400) + "..."); - - console.log(`\n[8/8] 测试 get_train_route_stations...`); - const ticketsResultForRoute = await get_tickets({ date: testDate, from_station: beijingnanCode, to_station: shanghaihongqiaoCode }); - const trainNoMatch = (ticketsResultForRoute as string).match(/train_no: (\w+)/); - if (trainNoMatch && trainNoMatch[1]) { - const trainNo = trainNoMatch[1]; - console.log(`使用车次 ${trainNo} 进行测试...`); - const routeStations = await get_train_route_stations({ - train_no: trainNo, - from_station_telecode: beijingnanCode, - to_station_telecode: shanghaihongqiaoCode, - depart_date: testDate - }); - console.log("测试结果:", JSON.stringify(routeStations, undefined, 2)); - } else { - console.log("未从 get_tickets 结果中找到可用车次来测试 get_train_route_stations。"); - } - - } catch (e: any) { - console.error("测试主函数出现错误:", e.message, e.stack); - complete({ success: false, message: `测试失败: ${e.message}` }); - return; - } - - console.log("\n--- 12306 工具包测试完成 ---"); - complete({ success: true, message: "所有测试已成功或已记录错误。" }); - } - - return { - get_current_date: (p: any) => wrap(get_current_date, p, '获取当前日期成功', '获取当前日期失败'), - get_stations_code_in_city: (p: any) => wrap(get_stations_code_in_city, p, '查询成功', '查询失败'), - get_station_code_of_citys: (p: any) => wrap(get_station_code_of_citys, p, '查询成功', '查询失败'), - get_station_code_by_names: (p: any) => wrap(get_station_code_by_names, p, '查询成功', '查询失败'), - get_station_by_telecode: (p: any) => wrap(get_station_by_telecode, p, '查询成功', '查询失败'), - get_tickets: (p: any) => wrap(get_tickets, p, '查询余票成功', '查询余票失败'), - get_interline_tickets: (p: any) => wrap(get_interline_tickets, p, '查询中转票成功', '查询中转票失败'), - get_train_route_stations: (p: any) => wrap(get_train_route_stations, p, '查询经停站成功', '查询经停站失败'), - main: main, - }; -})(); - -exports.get_current_date = ticket12306.get_current_date; -exports.get_stations_code_in_city = ticket12306.get_stations_code_in_city; -exports.get_station_code_of_citys = ticket12306.get_station_code_of_citys; -exports.get_station_code_by_names = ticket12306.get_station_code_by_names; -exports.get_station_by_telecode = ticket12306.get_station_by_telecode; -exports.get_tickets = ticket12306.get_tickets; -exports.get_interline_tickets = ticket12306.get_interline_tickets; -exports.get_train_route_stations = ticket12306.get_train_route_stations; -exports.main = ticket12306.main; \ No newline at end of file diff --git a/scripts/LICENSE b/scripts/LICENSE deleted file mode 100644 index 5519c44..0000000 --- a/scripts/LICENSE +++ /dev/null @@ -1,7 +0,0 @@ -# LGPL-3.0 License - -`scripts/` 目录下的文件来自 [Operit AI 项目](https://github.com/AAswordman/Operit),遵循 **GNU Lesser General Public License v3.0**。 - -完整许可证文本请参见: https://www.gnu.org/licenses/lgpl-3.0.html - -原项目仓库: https://github.com/AAswordman/Operit \ No newline at end of file diff --git a/scripts/README.md b/scripts/README.md deleted file mode 100644 index 3ef24f9..0000000 --- a/scripts/README.md +++ /dev/null @@ -1,84 +0,0 @@ -# Operit 脚本目录 - -此目录存放 **Operit AI 格式**的第三方工具脚本(`.js` / `.ts`)。 - -`operit_bridge` 插件会自动扫描此目录,解析所有带 `METADATA` 块的脚本, -并将它们注册为 xiaoli-cli 可调用的工具。 - -## 如何使用 - -### 1. 放入 Operit 脚本 - -将 Operit 格式的 `.js` 或 `.ts` 文件放入此目录即可。脚本必须包含 `/* METADATA ... */` 块。 - -```bash -# 从 Operit 社区下载脚本 -cp ~/Downloads/some_tool.js plugins/operit/ - -# 或从 Operit 仓库克隆 -git clone https://github.com/AAswordman/Operit.git /tmp/operit-repo -cp /tmp/operit-repo/examples/*.ts plugins/operit/ -``` - -### 2. 脚本格式要求 - -每个脚本头部必须有 METADATA 块: - -```javascript -/* -METADATA -{ - "name": "ToolName", - "display_name": { "zh": "工具名", "en": "Tool Name" }, - "description": "工具描述", - "author": ["作者"], - "category": "Utility", - "tools": [ - { - "name": "function_name", - "description": "功能描述", - "parameters": [ - { - "name": "param1", - "description": "参数说明", - "type": "string", - "required": true - } - ] - } - ] -} -*/ -``` - -### 3. 在 xiaoli-cli 中使用 - -``` -operit_bridge list # 查看已加载的 Operit 工具 -operit_bridge scan plugins/operit # 手动重新扫描 -operit_bridge info ToolName__function_name # 查看工具详情 -``` - -AI 也可以直接通过 Function Calling 调用这些工具。 - -## 支持的 category - -- `Automatic` - 自动化 -- `Chat` - 对话 -- `Development` - 开发 -- `Draw` - 绘图 -- `File` - 文件 -- `Life` - 生活 -- `Map` - 地图 -- `Media` - 媒体 -- `Memory` - 记忆 -- `Network` - 网络 -- `Search` - 搜索 -- `System` - 系统 -- `Utility` - 工具 -- `Workflow` - 工作流 - -## 参考 - -- Operit 脚本开发指南: https://github.com/AAswordman/Operit/blob/main/docs/SCRIPT_DEV_GUIDE.md -- Operit 示例脚本: https://github.com/AAswordman/Operit/tree/main/examples diff --git a/scripts/ai_chat.ts b/scripts/ai_chat.ts deleted file mode 100644 index f9a9e8f..0000000 --- a/scripts/ai_chat.ts +++ /dev/null @@ -1,458 +0,0 @@ -/* METADATA -{ - "name": "ai_chat", - - "display_name": { - "zh": "AI 相互对话", - "en": "AI-to-AI Chat" - }, - "description": { - "zh": "调用AI模型API实现AI之间智能对话互动。", - "en": "Call an AI model API to enable interactive conversations between AIs." - }, - "env": ["AI_API_BASE_URL", "AI_API_KEY", "AI_MODEL_NAME"], - "category": "Chat", - "tools": [{ - "name": "chat_completion", - "description": { - "zh": "发送消息给AI模型并获取回复。支持超时保护(默认30秒)。从根源上禁止分点列表输出。", - "en": "Send messages to an AI model and get responses. Includes timeout protection (default: 30s). Enforces non-bulleted output from the source." - }, - "parameters": [ - {"name": "messages", "description": {"zh": "消息数组或字符串", "en": "Message array or a string"}, "type": "any", "required": true}, - {"name": "system_prompt", "description": {"zh": "系统提示词(会自动追加禁止分点指令)", "en": "System prompt (the system will automatically append non-bulleted instructions)"}, "type": "string", "required": false}, - {"name": "temperature", "description": {"zh": "温度参数(0.0-2.0)", "en": "Temperature (0.0-2.0)"}, "type": "number", "required": false, "default": 0.7}, - {"name": "max_tokens", "description": {"zh": "最大生成长度", "en": "Maximum generation length"}, "type": "number", "required": false}, - {"name": "timeout", "description": {"zh": "超时时间(毫秒)", "en": "Timeout (milliseconds)"}, "type": "number", "required": false, "default": 30000} - ] - }] - } */ - -/// - -const aiModelInteraction = (function () { - type ChatRole = 'system' | 'user' | 'assistant' | 'function'; - - type ChatMessage = { - role: ChatRole; - content: string; - name?: string; - function_call?: { - name: string; - arguments?: string; - }; - }; - - type ChatCompletionToolParams = { - messages: ChatMessage[] | string; - message?: string; - system_prompt?: string; - temperature?: number; - max_tokens?: number; - timeout?: number; - functions?: any[]; - }; - - type UniversalHttpResponse = { - status: number; - statusText: string; - headers?: Record; - body: any; - }; - - async function universalHttpRequest( - url: string, - method: string = 'POST', - headers: Record = {}, - body: any = null, - responseType: 'json' | 'text' = 'json', - timeout: number = 30000 - ): Promise { - const timeoutPromise: Promise = new Promise((_, reject) => - setTimeout(() => reject(new Error(`请求超时:${timeout}ms`)), timeout) - ); - - const requestPromise: Promise = (async () => { - if (typeof OkHttp === 'undefined') { - throw new Error('OkHttp 不可用'); - } - - const client = OkHttp.newBuilder() - .connectTimeout(timeout) - .readTimeout(timeout) - .writeTimeout(timeout) - .build(); - - const requestBuilder = client.newRequest() - .url(url) - .method(method.toUpperCase()); - - if (headers && Object.keys(headers).length > 0) { - requestBuilder.headers(headers); - } - - const upperMethod = method.toUpperCase(); - if (body !== null && body !== undefined && upperMethod !== 'GET' && upperMethod !== 'HEAD') { - if (typeof body === 'string') { - requestBuilder.body(body, 'text'); - } else { - requestBuilder.body(body, 'json'); - } - } - - const response: OkHttpResponse = await requestBuilder.build().execute(); - const resultBody = responseType === 'json' ? response.json() : response.content; - return { - status: response.statusCode, - statusText: response.statusMessage || '', - headers: response.headers, - body: resultBody, - }; - })(); - - return Promise.race([requestPromise, timeoutPromise]); - } - - function cleanText(text: unknown): string { - if (!text || typeof text !== 'string') return String(text ?? ''); - - let cleaned: string = text; - - // 处理转义与实体 - cleaned = cleaned.replace(/\|["\\]?n/g, '\n'); - cleaned = cleaned.replace(/\\\\([\\nrt"'&])/g, (m, c) => ({ n: '\n', r: '\r', t: '\t', '"': '"', "'": "'", "&": "&" }[c] || m)); - cleaned = cleaned.replace(/\\u([0-9A-Fa-f]{4})/g, (_, h) => String.fromCharCode(parseInt(h, 16))); - - // 一次性替换HTML实体 - const entities = { quot: '"', amp: '&', lt: '<', gt: '>', nbsp: ' ', '#39': "'", apos: "'" }; - cleaned = cleaned.replace(/&(\w+|#\d+);/g, (m, e) => entities[e] || m); - - // 清理代码块 - cleaned = cleaned.replace(/```[\w-]*\s*\n([\s\S]*?)```/g, '$1').replace(/```/g, '').replace(/`([^`]+)`/g, '$1'); - - // 格式化空白与分点 - cleaned = cleaned.replace(/[ \t]{2,}/g, ' '); - cleaned = cleaned.split('\n').map(l => l.replace(/^[\s\uFEFF\xA0\u3000\u200B-\u200D]+/g, '')).join('\n'); - cleaned = cleaned.replace(/\n{3,}/g, '\n\n').trim(); - cleaned = cleaned.replace(/^\s*[-•●]\s+|^\s*\d+\.\s+|^\s*\d+\)\s+|^\s*\([a-zA-Z]\)\s+/gm, ''); - - return cleaned || text; - } - - function getConfig(varName: string, defaultValue: string | null = null): string { - try { - const value = getEnv(varName); - if (!value || value === `YOUR_${varName}`) { - if (defaultValue !== null) return defaultValue; - throw new Error(`${varName} 未配置`); - } - return value.trim(); - } catch (e) { - if (defaultValue !== null) return defaultValue; - throw new Error(`${varName} 未配置`); - } - } - - function getFullConfig() { - try { - return { - apiBaseUrl: getConfig('AI_API_BASE_URL', ''), - apiKey: getConfig('AI_API_KEY', ''), - modelName: getConfig('AI_MODEL_NAME', ''), - timeout: 30000 - }; - } catch { - return { apiBaseUrl: '', apiKey: '', modelName: '', timeout: 30000 }; - } - } - - function joinUrl(baseUrl: string, path: string): string { - const normalizedBase = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`; - const normalizedPath = path.startsWith('/') ? path.slice(1) : path; - return `${normalizedBase}${normalizedPath}`; - } - - async function tryEndpoints(baseUrl: string, payload: Record, config: { apiKey: string }, timeout: number): Promise { - if (baseUrl.includes('chat/completions')) { - return await universalHttpRequest( - baseUrl, - 'POST', - { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${config.apiKey}` - }, - payload, - 'json', - timeout - ); - } - - const endpoints = ['v1/chat/completions', 'chat/completions']; - let lastError; - - for (const endpoint of endpoints) { - try { - const url = joinUrl(baseUrl, endpoint); - const response = await universalHttpRequest( - url, - 'POST', - { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${config.apiKey}` - }, - payload, - 'json', - timeout - ); - if (response.status === 200) return response; - lastError = new Error(`端点 ${endpoint} 返回 ${response.status}`); - } catch (error) { - lastError = error; - } - } - - throw lastError || new Error('所有端点尝试失败'); - } - - async function chat_completion_logic(rawInput: ChatCompletionToolParams | string, timeout: number) { - if (rawInput === null || rawInput === undefined) { - throw new Error('参数错误: 输入参数不能为空'); - } - - const internalParams: { - messages: ChatMessage[]; - system_prompt?: string; - temperature: number; - max_tokens?: number; - functions?: any[]; - } = { - messages: [], - system_prompt: undefined, - temperature: 0.7, - max_tokens: undefined, - functions: undefined, - }; - - // 处理输入参数 - if (typeof rawInput === 'string') { - internalParams.messages = [{ role: 'user', content: rawInput }]; - } else if (typeof rawInput === 'object') { - const hasMessageField = 'message' in rawInput; - const hasMessagesField = 'messages' in rawInput; - - if (hasMessagesField) { - const messagesValue = (rawInput as ChatCompletionToolParams).messages; - if (Array.isArray(messagesValue)) { - internalParams.messages = messagesValue as ChatMessage[]; - } else if (typeof messagesValue === 'string') { - internalParams.messages = [{ role: 'user', content: messagesValue }]; - } else { - throw new Error(`'messages' 必须是数组或字符串`); - } - } else if (hasMessageField) { - const messageValue = (rawInput as ChatCompletionToolParams).message; - if (typeof messageValue !== 'string') { - throw new Error(`'message' 必须是字符串`); - } - internalParams.messages = [{ role: 'user', content: messageValue }]; - } else { - throw new Error(`对象必须包含 'message' 或 'messages' 字段`); - } - - internalParams.system_prompt = (rawInput as ChatCompletionToolParams).system_prompt ? String((rawInput as ChatCompletionToolParams).system_prompt) : undefined; - internalParams.temperature = (rawInput as ChatCompletionToolParams).temperature !== undefined ? Number((rawInput as ChatCompletionToolParams).temperature) : 0.7; - if (isNaN(internalParams.temperature)) internalParams.temperature = 0.7; - - internalParams.max_tokens = (rawInput as ChatCompletionToolParams).max_tokens !== undefined ? Number((rawInput as ChatCompletionToolParams).max_tokens) : undefined; - if (internalParams.max_tokens !== undefined && isNaN(internalParams.max_tokens)) { - internalParams.max_tokens = undefined; - } - - internalParams.functions = Array.isArray((rawInput as ChatCompletionToolParams).functions) ? (rawInput as ChatCompletionToolParams).functions : undefined; - } else { - throw new Error(`不支持的参数类型 '${typeof rawInput}'`); - } - - // 验证messages - if (!Array.isArray(internalParams.messages) || internalParams.messages.length === 0) { - throw new Error('messages必须是有效数组且不为空'); - } - - internalParams.messages.forEach((msg, idx) => { - if (!msg || typeof msg !== 'object' || !msg.role || typeof msg.content !== 'string') { - throw new Error(`消息 #${idx} 格式无效`); - } - }); - - // 获取配置 - const config = getFullConfig(); - if (!config.apiBaseUrl || !config.apiKey) { - throw new Error('AI_API_BASE_URL 和 AI_API_KEY 必须配置'); - } - - // 构建消息数组 - const antiListInstruction = "【重要指令】你必须以连续段落的方式回答,严禁使用任何分点、列表、编号或项目符号格式。"; - const finalMessages = [ - { - role: 'system', - content: internalParams.system_prompt - ? internalParams.system_prompt + "\n\n" + antiListInstruction - : antiListInstruction - }, - ...internalParams.messages - ]; - - // 构建请求 - const payload: Record = { - model: String(config.modelName || 'gpt-3.5-turbo'), - messages: finalMessages, - temperature: Number(internalParams.temperature) - }; - - if (internalParams.max_tokens !== undefined && !isNaN(internalParams.max_tokens)) { - payload.max_tokens = Number(internalParams.max_tokens); - } - if (internalParams.functions) payload.functions = internalParams.functions; - - Object.keys(payload).forEach(key => payload[key] === undefined && delete payload[key]); - - // 发送请求 - let response; - try { - response = await tryEndpoints(config.apiBaseUrl, payload, config, timeout); - } catch { - response = await universalHttpRequest( - config.apiBaseUrl, - 'POST', - { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${config.apiKey}` - }, - payload, - 'json', - timeout - ); - } - - if (response.status !== 200) { - let errorMsg = `API请求失败: ${response.status}`; - try { - const errorData = typeof response.body === 'string' ? JSON.parse(response.body) : response.body; - errorMsg += ` - ${errorData.error?.message || JSON.stringify(errorData)}`; - } catch { - errorMsg += ` - ${response.statusText}`; - } - throw new Error(errorMsg); - } - - const result = typeof response.body === 'string' ? JSON.parse(response.body) : response.body; - if (!result.choices?.[0]) { - throw new Error('API返回格式异常'); - } - - const choice = result.choices[0]; - const reply: any = { - message: choice.message, - finish_reason: choice.finish_reason, - usage: result.usage || null, - model: result.model || config.modelName, - id: result.id, - created: result.created - }; - - if (choice.message.function_call) { - reply.is_function_call = true; - reply.function_name = choice.message.function_call.name; - reply.function_arguments = JSON.parse(choice.message.function_call.arguments || '{}'); - } - - return reply; - } - - async function chat_completion_impl(params: ChatCompletionToolParams | string) { - const normalizedParams: ChatCompletionToolParams = typeof params === 'object' - ? (params as ChatCompletionToolParams) - : { messages: String(params) }; - - const timeout = normalizedParams.timeout || 30000; - - const result = await Promise.race([ - chat_completion_logic(normalizedParams, timeout), - new Promise((_, reject) => - setTimeout(() => reject(new Error(`操作超时:${timeout}ms`)), timeout) - ) - ]); - - const rawReply = result.message?.content; - const cleanedReply = cleanText(rawReply); - - return { - success: true, - message: "AI回复获取成功!", - data: result, - reply: cleanedReply, - raw_reply: rawReply, - usage: result.usage, - anti_list_applied: true - }; - } - - async function wrapToolExecution(func: (params: any) => Promise, params?: any) { - try { - const result = await func(params || {}); - complete(result); - } catch (error: any) { - console.error(`Tool ${func.name} failed unexpectedly`, error); - complete({ - success: false, - message: `AI对话失败: ${String(error && error.message ? error.message : error)}`, - error_stack: error && error.stack - }); - } - } - - async function chat_completion(params?: ChatCompletionToolParams | string) { - return await wrapToolExecution(chat_completion_impl, params); - } - - async function main(params?: { message?: string; temperature?: number; timeout?: number }) { - const config = getFullConfig(); - if (!config.apiBaseUrl || !config.apiKey) { - return { - success: false, - message: 'AI_API_BASE_URL 和 AI_API_KEY 必须配置', - config - }; - } - - return await chat_completion_impl({ - messages: params?.message || 'Hello! Please respond in a continuous paragraph without any list or bullet points.', - temperature: params?.temperature ?? 0.2, - timeout: params?.timeout ?? 15000, - }); - } - - return { - chat_completion, - single_message: chat_completion, - test_connection: async (params?: { timeout?: number }) => wrapToolExecution( - async () => await chat_completion_impl({ - messages: "Hello! Please respond in a continuous paragraph without any list or bullet points.", - temperature: 0.1, - timeout: params?.timeout ?? 10000 - }), - {} - ), - main: (params?: { message?: string; temperature?: number; timeout?: number }) => wrapToolExecution(main, params), - getConfig: getFullConfig, - _makeHttpRequest: universalHttpRequest, - _cleanText: cleanText - }; -})(); - -// 导出工具函数(CommonJS) -exports.chat_completion = aiModelInteraction.chat_completion; -exports.single_message = aiModelInteraction.single_message; -exports.test_connection = aiModelInteraction.test_connection; -exports.getConfig = aiModelInteraction.getConfig; -exports.main = aiModelInteraction.main; diff --git a/scripts/automatic_baidu_map_assistant.ts b/scripts/automatic_baidu_map_assistant.ts deleted file mode 100644 index 40548d9..0000000 --- a/scripts/automatic_baidu_map_assistant.ts +++ /dev/null @@ -1,633 +0,0 @@ -/* -METADATA -{ - // 百度地图智能助手 - name: "Automatic_baiduMap_assistant", - display_name: { - zh: "百度地图自动化助手" - en: "Baidu Map Automation Assistant" - }description: { zh: "高级百度地图智能助手,通过UI自动化技术实现地图交互,支持地点搜索、路线规划、周边查询等功能,为AI提供强大的地理位置服务能力。适用于出行规划、信息查询、智能问答等场景。", en: "Advanced Baidu Maps assistant powered by UI automation. Supports place search, navigation/route planning, and nearby discovery, enabling AI-driven location services. Useful for trip planning, information lookup, and Q&A." }, - - // Tools in this package - category: "Automatic" - tools: [ - { - name: "workflow_guide", - description: { zh: "百度地图助手工具使用流程指南。要完成复杂任务,请按以下顺序组合使用工具:\n1. **搜索地点**: 使用 `search_location` 查找地点、餐馆、公司等,这是多数操作的起点。\n2. **选择地点**: 如果 `search_location` 返回多个结果,使用 `select_location_from_list` 从列表中选择一个。\n3. **开始导航**: 搜索到唯一地点或从列表中选择地点后,使用 `go_to_location` 来规划到该地的路线。\n4. **周边搜索**: 使用 `search_nearby` 查找当前位置或指定地点附近的设施。\n- **随时导航**: `navigate_to_home` 可随时返回地图主页。", en: "Workflow guide for the Baidu Maps assistant. For complex tasks, combine tools in this order:\n1. **Search**: use `search_location` to find places/restaurants/companies (starting point for most tasks).\n2. **Select**: if `search_location` returns multiple results, use `select_location_from_list` to pick one from the list.\n3. **Navigate**: after a unique result is found or selected, use `go_to_location` to plan a route.\n4. **Nearby**: use `search_nearby` to find facilities near your current location or a specified place.\n- **Go home anytime**: use `navigate_to_home` to return to the map home screen." } - parameters: [] - advice: true - }, - { - name: "search_location", - description: { zh: "在百度地图中搜索地点、餐馆、公司等信息。", en: "Search places, restaurants, companies, and other POIs in Baidu Maps." }, - parameters: [ - { - name: "keyword", - description: { zh: "要搜索的地点关键词", en: "Keyword of the place to search for." }, - type: "string", - required: true - } - ] - }, - { - name: "get_directions", - description: { zh: "规划从起点到终点的路线。", en: "Plan a route from a start point to a destination." }, - parameters: [ - { - name: "start_point", - description: { zh: "路线起点,默认为'我的位置'", en: "Route start point (default: 'My location')." }, - type: "string", - required: false - }, - { - name: "end_point", - description: { zh: "路线终点", en: "Route destination." }, - type: "string", - required: true - }, - { - name: "transport_mode", - description: { zh: "交通方式:driving(驾车), transit(公交), walking(步行), cycling(骑行)", en: "Transport mode: driving, transit, walking, cycling." }, - type: "string", - required: false - } - ] - }, - { - name: "select_location_from_list", - description: { zh: "从 'search_location' 返回的搜索结果列表中选择一个地点。", en: "Select a place from the result list returned by `search_location`." }, - parameters: [ - { - name: "keyword", - description: { zh: "要选择的地点名称关键词", en: "Keyword to match the place name to select." }, - type: "string", - required: false - }, - { - name: "index", - description: { zh: "要选择的地点在列表中的索引位置(从1开始)", en: "Index of the item in the list (1-based)." }, - type: "number", - required: false - } - ] - }, - { - name: "go_to_location", - description: { zh: "在地点详情页点击“到这去”,以开启导航或路线规划。可以选择交通方式。", en: "On the place detail page, tap \"Go\" (到这去) to start navigation/route planning. You can optionally select a transport mode." }, - parameters: [ - { - name: "transport_mode", - description: { zh: "交通方式:新能源, 驾车, 打车, 公共交通, 代驾, 骑行, 步行, 拼车, 飞机, 火车, 客车, 摩托车", en: "Transport mode tab label in the app (e.g., 新能源, 驾车, 打车, 公共交通, 代驾, 骑行, 步行, 拼车, 飞机, 火车, 客车, 摩托车)." }, - type: "string", - required: false - } - ] - }, - { - name: "search_nearby", - description: { zh: "搜索中心点附近的地点信息。", en: "Search places around a center point." }, - parameters: [ - { - name: "keyword", - description: { zh: "要搜索的周边设施关键词(如:银行, 加油站)", en: "Nearby keyword (e.g., bank, gas station)." }, - type: "string", - required: true - }, - { - name: "center_point", - description: { zh: "搜索的中心点,默认为'我的位置'", en: "Center point for the search (default: 'My location')." }, - type: "string", - required: false - } - ] - }, - { - name: "navigate_to_home", - description: { zh: "返回百度地图的主界面。", en: "Return to the Baidu Maps home screen." }, - parameters: [] - }, - { - name: "activate_voice_assistant", - description: { zh: "在主界面点击语音搜索按钮,启动“小度”语音助手。", en: "Tap the voice search button on the home screen to launch the Xiaodu voice assistant." }, - parameters: [] - } - ] -}*/ - -interface Array { - at(index: number): T | undefined; -} - -const BaiduMapAssistant = (function () { - - // 添加 Array.prototype.at 支持 - if (!Array.prototype.at) { - Object.defineProperty(Array.prototype, 'at', { - value: function (index: number) { - if (index < 0) { - index += this.length; - } - return this[index]; - }, - enumerable: false, - configurable: true, - writable: true - }); - } - - // 百度地图应用包名 - const BAIDU_MAP_PACKAGE = "com.baidu.BaiduMap"; - const MAIN_ACTIVITY = "com.baidu.baidumaps.MapsActivity"; - - // Helper to create response objects - function createResponse(success: boolean, message: string, data: object | string = {}) { - if (typeof data === 'string') { - return { success, message, data }; - } - return { success, message, ...data }; - } - - // Helper to find a UI element and click it - async function findAndClick(finder: () => Promise): Promise { - const element = await finder(); - if (element) { - await element.click(); - await Tools.System.sleep(1000); - return true; - } - return false; - } - - // Helper to find a UI element by text and click it - async function findAndClickByText(text: string): Promise { - return findAndClick(async () => (await UINode.getCurrentPage()).findByText(text)); - } - - async function isOnMainPage(): Promise { - const page = await UINode.getCurrentPage(); - // Check for the presence of key elements that indicate the main map screen. - const drivingButton = page.findByText("驾车"); - const transitButton = page.findByText("公共交通"); - const taxiButton = page.findByText("打车"); - const hotelButton = page.findByText("订酒店"); - - if (drivingButton && transitButton && taxiButton && hotelButton) { - console.log("Detected Baidu Map main page by key elements."); - return true; - } - console.log("Not on main page, key elements not found."); - return false; - } - - async function ensureMain(packageName: string = BAIDU_MAP_PACKAGE): Promise { - let pageInfo = await Tools.UI.getPageInfo(); - - // 1. If not in the correct app, start it. - if (pageInfo.packageName !== packageName) { - console.log(`Not in the correct app. Current: ${pageInfo.packageName}. Starting ${packageName}.`); - await Tools.System.startApp(packageName); - await Tools.System.sleep(5000); // Wait for app to load - pageInfo = await Tools.UI.getPageInfo(); // Re-check page info - if (pageInfo.packageName !== packageName) { - console.error(`Failed to start ${packageName}.`); - return false; - } - } - - // 2. Check if we are already on the main page. - if (await isOnMainPage()) { - console.log("Already on the main page."); - return true; - } - - // 3. If not on the main page, try pressing back up to 4 times. - console.log("Not on the main page. Attempting to go back."); - for (let i = 0; i < 4; i++) { - await Tools.UI.pressKey("KEYCODE_BACK"); - await Tools.System.sleep(1000); // Wait for UI to settle - - pageInfo = await Tools.UI.getPageInfo(); - // If we have exited the app, stop trying. - if (pageInfo.packageName !== packageName) { - console.log("Exited the app while trying to go back."); - break; // Exit the loop, will proceed to restart - } - - if (await isOnMainPage()) { - console.log(`Successfully returned to main page on attempt ${i + 1}.`); - return true; - } - } - - // 4. If pressing back failed, perform a full app restart. - console.log("Pressing back failed or exited app. Performing a full app restart."); - await Tools.System.stopApp(packageName); - await Tools.System.sleep(1000); - await Tools.System.startApp(packageName); - await Tools.System.sleep(5000); // Longer wait for cold start - - pageInfo = await Tools.UI.getPageInfo(); - if (pageInfo.packageName !== packageName) { - console.error(`Failed to restart ${packageName}.`); - return false; - } - - if (await isOnMainPage()) { - console.log("Successfully reached main page after restart."); - return true; - } - - console.error(`Failed to navigate to the main page of ${packageName} even after restart.`); - return false; - } - - async function search_location(params: { keyword: string }): Promise { - const { keyword } = params; - console.log(`Searching for location: ${keyword}`); - - // 确保百度地图已在前台运行 - if (!await ensureMain()) { - return createResponse(false, "无法启动或切换到百度地图"); - } - - await Tools.System.sleep(2000); // 等待首页完全加载 - - // 查找并点击搜索框 - const page = await UINode.getCurrentPage(); - const searchBox = page.findById('serachbox_container'); - if (!searchBox) { - return createResponse(false, "在地图主页未找到搜索框 (ID: serachbox_container)"); - } - await searchBox.click(); - await Tools.System.sleep(300); - await Tools.UI.setText(keyword); - await Tools.System.sleep(300); - - - // 点击搜索按钮 - const searchButton = (await UINode.getCurrentPage()).findByText("搜索"); - if (searchButton) { - await searchButton.click(); - } - - await Tools.System.sleep(5000); // 等待搜索结果加载 - - // 上拉以显示完整的结果列表,这在某些情况下是必要的 - console.log("执行上拉手势以确保结果列表完全可见..."); - await Tools.UI.swipe(540, 1800, 540, 900); // 从屏幕底部向上滑动 - await Tools.System.sleep(1500); // 等待动画完成 - - // 检查页面是唯一结果还是列表 - const pageAfterSearch = await UINode.getCurrentPage(); - const listContainer = pageAfterSearch.findById("com.baidu.BaiduMap:id/talosListContainer"); - - if (listContainer) { - // 情况2: 结果列表 - console.log("检测到列表容器,开始解析结果列表..."); - const results = await get_map_search_results(); - - return createResponse(true, `搜索到 ${results.length} 个相关地点。请使用 'select_location_from_list' 选择一个。`, { - keyword: keyword, - results: results.map(({ element, ...rest }) => rest), // 移除 element 属性 - result_count: results.length - }); - } - - const goToButton = pageAfterSearch.findByText("到这去"); - if (goToButton) { - // 情况1: 唯一结果 - return createResponse(true, `已找到唯一结果 "${keyword}"。您现在可以使用 'go_to_location' 进行导航。`, { keyword }); - } else { - return createResponse(false, `搜索 "${keyword}" 后既未找到结果列表,也未找到唯一结果。`); - } - } - - async function get_map_search_results(desiredCount: number = 20): Promise { - const results: any[] = []; - const seenTitles = new Set(); - const maxScrolls = 1; - let lastResultCount = -1; - - for (let i = 0; i < maxScrolls; i++) { - let page = await UINode.getCurrentPage(); - const resultsContainer = page.findById('com.baidu.BaiduMap:id/talosListContainer'); - if (!resultsContainer) { - console.log("未找到搜索结果容器 (ID: com.baidu.BaiduMap:id/talosListContainer)"); - break; - } - - const recyclerView = resultsContainer.findByClass("RecyclerView"); - if (!recyclerView) { - console.log("未在结果容器中找到 RecyclerView。"); - break; - } - - // 通过查找所有“到这去”按钮来定位每个列表项 - const goToButtons = recyclerView.findAllByText("到这去"); - - for (const button of goToButtons) { - // 找到按钮所属的、作为RecyclerView直接子节点的那个父节点 - const item = button.closest((node) => - node.parent !== undefined && node.parent.equals(recyclerView) - ); - if (!item) continue; - - const textViews = item.findAllByClass('TextView'); - const relevantTextViews = textViews.filter(tv => tv.text && tv.text.trim() !== '到这去' && tv.text.trim() !== ''); - - if (relevantTextViews.length === 0) continue; - - // 按纵坐标排序,最上面的通常是标题 - relevantTextViews.sort((a, b) => { - try { - const [, y1a] = a.bounds!.match(/\[\d+,(\d+)\]/)!; - const [, y1b] = b.bounds!.match(/\[\d+,(\d+)\]/)!; - return parseInt(y1a) - parseInt(y1b); - } catch (e) { - return 0; - } - }); - - const title = relevantTextViews[0].text; - if (!title || seenTitles.has(title)) { - continue; - } - seenTitles.add(title); - - // 查找包含地址特征的文本作为地址 - const addressNode = relevantTextViews.find(tv => - tv.text && (tv.text.includes('省') || tv.text.includes('市') || tv.text.includes('区') || tv.text.includes('路') || tv.text.includes('号')) - ); - const address = addressNode ? addressNode.text : "N/A"; - - results.push({ - index: results.length + 1, // 临时索引 - title: title, - address: address, - element: item - }); - } - - // 滚动前去重并检查是否需要停止 - const uniqueResults = Array.from(new Map(results.map(r => [r.title, r])).values()); - - if (uniqueResults.length >= desiredCount) { - console.log(`已找到 ${uniqueResults.length} 条不重复结果,满足要求。`); - break; - } - if (uniqueResults.length === lastResultCount) { - console.log("滚动未产生新结果,停止滚动。"); - break; - } - lastResultCount = uniqueResults.length; - - // 向下滚动加载更多 - await Tools.UI.swipe(540, 1800, 540, 600); - await Tools.System.sleep(2000); // 增加等待时间以确保加载 - } - - // 最终去重并重新编号索引 - const finalResults = Array.from(new Map(results.map(item => [item.title, item])).values()) - .map((item, index) => ({ ...item, index: index + 1 })); - - return finalResults.slice(0, desiredCount); - } - - // --- Placeholder functions for other tools --- - async function select_location_from_list(params: { keyword?: string, index?: number }): Promise { - const { keyword, index } = params; - - if (!keyword && !index) { - return createResponse(false, "请提供地点关键词或索引。"); - } - console.log(`尝试从列表中选择 - 关键词: "${keyword}", 索引: ${index}`); - - const seenTitles = new Set(); - let currentItemCount = 0; - const maxScrolls = 10; - let lastSeenCount = -1; - - for (let i = 0; i < maxScrolls; i++) { - const page = await UINode.getCurrentPage(); - // In Baidu Maps, the results are often inside a RecyclerView - const recyclerView = page.findByClass("RecyclerView"); - if (!recyclerView) { - console.log("未找到 RecyclerView, 无法选择项目。"); - return createResponse(false, "未找到结果列表容器 (RecyclerView)。"); - } - - const goToButtons = recyclerView.findAllByText("到这去"); - - for (const button of goToButtons) { - const item = button.closest((node) => - node.parent !== undefined && node.parent.equals(recyclerView) - ); - if (!item) continue; - - const textViews = item.findAllByClass('TextView'); - const relevantTextViews = textViews.filter(tv => tv.text && tv.text.trim() !== '到这去' && tv.text.trim() !== ''); - if (relevantTextViews.length === 0) continue; - - relevantTextViews.sort((a, b) => { - try { - const [, y1a] = a.bounds!.match(/\[\d+,(\d+)\]/)!; - const [, y1b] = b.bounds!.match(/\[\d+,(\d+)\]/)!; - return parseInt(y1a) - parseInt(y1b); - } catch (e) { return 0; } - }); - - const title = relevantTextViews[0].text; - if (!title || seenTitles.has(title)) { - continue; - } - - seenTitles.add(title); - currentItemCount++; - - let shouldClick = false; - if (index) { - if (currentItemCount === index) { - shouldClick = true; - } - } else if (keyword) { - if (title.includes(keyword)) { - shouldClick = true; - } - } - - if (shouldClick) { - await item.click(); - await Tools.System.sleep(2000); - return createResponse(true, `已成功选择地点: "${title}"`); - } - } - - if (seenTitles.size === lastSeenCount) { - console.log("滚动未产生新结果,停止。"); - break; - } - lastSeenCount = seenTitles.size; - - if (index && currentItemCount >= index) { - // If we're looking for an index and have passed it, no need to scroll more. - break; - } - - await Tools.UI.swipe(540, 1800, 540, 600); // Swipe up - await Tools.System.sleep(2000); - } - - return createResponse(false, "在列表中未找到或无法选择指定的地点。", { keyword, index }); - } - - async function go_to_location(params: { transport_mode?: string }): Promise { - const transportMode = params.transport_mode || "驾车"; // 默认为驾车 - console.log("尝试点击 '到这去' 按钮..."); - const page = await UINode.getCurrentPage(); - const goToButton = page.findByText("到这去"); - if (goToButton) { - await goToButton.click(); - await Tools.System.sleep(3000); // 等待路线规划页面加载 - - if (transportMode) { - console.log(`尝试选择交通方式: ${transportMode}`); - let pageAfterClick = await UINode.getCurrentPage(); - - // 尝试直接点击可见的交通方式按钮 - try { - let modeButton = pageAfterClick.findByText(transportMode); - if (modeButton) { - await modeButton.click(); - await Tools.System.sleep(1000); - return createResponse(true, `已点击“到这去”,并成功切换到“${transportMode}”模式。`); - } - } catch (e) { - console.log(`直接点击失败,按钮可能在屏幕外。尝试滚动查找。`); - } - - // 如果未直接找到,尝试展开更多选项 - console.log(`未直接找到 "${transportMode}",尝试展开更多选项...`); - const moreModesButton = pageAfterClick.findById('route_tab_group'); - if (moreModesButton) { - await moreModesButton.click(); - await Tools.System.sleep(1500); // 等待展开动画 - pageAfterClick = await UINode.getCurrentPage(); // 刷新页面节点 - let modeButton = pageAfterClick.findByText(transportMode); - if (modeButton) { - await modeButton.click(); - await Tools.System.sleep(1500); - return createResponse(true, `已点击“到这去”,并成功切换到“${transportMode}”模式。`); - } - } - - return createResponse(false, `已点击“到这去”,但无法找到交通方式“${transportMode}”。`); - } - - return createResponse(true, "已点击“到这去”,进入路线规划页面。"); - } else { - return createResponse(false, "在当前页面未找到“到这去”按钮。"); - } - } - - async function get_directions(params: { start_point?: string, end_point: string, transport_mode?: string }): Promise { - return createResponse(false, "功能 'get_directions' 尚未实现。"); - } - - async function search_nearby(params: { keyword: string, center_point?: string }): Promise { - return createResponse(false, "功能 'search_nearby' 尚未实现。"); - } - - async function navigate_to_home(params: {}): Promise { - console.log("Navigating to home screen."); - if (await ensureMain()) { - // 在地图应用中,通常按返回键可以回到主图区 - // 这里可以根据实际情况增加返回逻辑 - await Tools.UI.pressKey("KEYCODE_BACK"); - await Tools.System.sleep(500); - await Tools.UI.pressKey("KEYCODE_BACK"); - return createResponse(true, "已尝试返回主界面。"); - } else { - return createResponse(false, "无法导航到主界面,因为无法启动百度地图。"); - } - } - - async function activate_voice_assistant(params: {}): Promise { - console.log("正在激活语音助手“小度”..."); - if (!await ensureMain()) { - return createResponse(false, "无法启动或切换到百度地图主页,因此无法激活语音助手。"); - } - await Tools.System.sleep(1000); // 等待UI稳定 - - const page = await UINode.getCurrentPage(); - const voiceButton = page.findById('voice_search'); - - if (voiceButton) { - await voiceButton.click(); - await Tools.System.sleep(2000); // 等待语音助手界面弹出 - return createResponse(true, "已成功激活“小度”语音助手。"); - } else { - return createResponse(false, "在主界面上未找到语音搜索按钮 (ID: voice_search)。"); - } - } - - async function wrapToolExecution(func: (params: any) => Promise, params: any) { - try { - const result = await func(params); - complete({ - ...result, - }); - } catch (error) { - console.error(`Tool ${func.name} failed unexpectedly`, error); - complete({ - success: false, - message: `工具执行时发生意外错误: ${error.message}`, - }); - } - } - - async function main(params: {}) { - console.log("Running main for testing..."); - // 这是一个用于测试的函数,它会搜索一个默认位置 - // console.log("--- Test Case 1: Searching for '上海' ---"); - // await search_location({ keyword: "上海" }); - // await Tools.System.sleep(2000); // Wait for page to load - - // // console.log("--- Test Case 1: Clicking 'go_to_location' ---"); - // await go_to_location({ "transport_mode": "飞机" }); - // await Tools.System.sleep(2000); // Wait for action - - // // Navigate back to home for the next test - await navigate_to_home({}); - await Tools.System.sleep(3000); - - console.log("--- Test Case 2: Searching for '长安大学' to test multiple results ---"); - await search_location({ keyword: "长安大学" }); - await Tools.System.sleep(2000); - await select_location_from_list({ index: 1 }); - await Tools.System.sleep(2000); - await go_to_location({ transport_mode: "飞机" }); - await Tools.System.sleep(2000); - } - - // 导出所有工具 - return { - search_location: async (params) => await wrapToolExecution(search_location, params), - select_location_from_list: async (params) => await wrapToolExecution(select_location_from_list, params), - go_to_location: async (params) => await wrapToolExecution(go_to_location, params), - get_directions: async (params) => await wrapToolExecution(get_directions, params), - search_nearby: async (params) => await wrapToolExecution(search_nearby, params), - navigate_to_home: async (params) => await wrapToolExecution(navigate_to_home, params), - activate_voice_assistant: async (params) => await wrapToolExecution(activate_voice_assistant, params), - main: async (params) => await wrapToolExecution(main, params), - }; -})(); - -// 逐个导出,以便在外部调用 -exports.search_location = BaiduMapAssistant.search_location; -exports.select_location_from_list = BaiduMapAssistant.select_location_from_list; -exports.go_to_location = BaiduMapAssistant.go_to_location; -exports.get_directions = BaiduMapAssistant.get_directions; -exports.search_nearby = BaiduMapAssistant.search_nearby; -exports.navigate_to_home = BaiduMapAssistant.navigate_to_home; -exports.activate_voice_assistant = BaiduMapAssistant.activate_voice_assistant; -exports.main = BaiduMapAssistant.main; \ No newline at end of file diff --git a/scripts/automatic_bilibili_assistant.ts b/scripts/automatic_bilibili_assistant.ts deleted file mode 100644 index 678ca50..0000000 --- a/scripts/automatic_bilibili_assistant.ts +++ /dev/null @@ -1,918 +0,0 @@ -/* -METADATA -{ - // Bilibili 智能助手包 - name: Automatic_bilibili_assistant - - display_name: { - zh: "Bilibili自动化助手" - en: "Bilibili Automation Assistant" - }description: { zh: "高级B站智能助手,通过UI自动化技术实现B站应用交互,支持视频搜索播放、评论互动、用户关注等功能,为AI赋予B站社交和内容消费能力。适用于自动追番、视频推荐、社交互动等场景。", en: "Advanced Bilibili assistant powered by UI automation. Supports video search/playback, commenting, and following uploaders, enabling AI-driven content consumption and social interaction on Bilibili." } - - // Tools in this package - category: "Automatic" - tools: [ - { - name: workflow_guide - description: { zh: "B站助手工具使用流程指南。要完成复杂任务,请按以下顺序组合使用工具:\n1. **搜索视频**: 使用 `search_video` 查找视频,这是所有操作的起点。\n2. **播放视频**: 从搜索结果中,使用 `play_video` 播放指定视频。\n3. **视频内互动**: 视频播放后,可执行 `get_video_info`, `like_video`, `collect_video`, `follow_uploader`, `send_comment`, `browse_comments` 等操作。\n4. **返回列表**: 要播放另一个视频,必须先用 `return_to_video_list` 返回搜索列表,再调用 `play_video`。\n- **随时导航**: `navigate_to_home` 等导航工具可在多数情况下使用。", en: "Workflow guide for the Bilibili assistant:\n1. **Search videos**: use `search_video` as the entry point for most tasks.\n2. **Play a video**: use `play_video` to open a specific result.\n3. **Interact**: while on the video page you can run `get_video_info`, `like_video`, `collect_video`, `follow_uploader`, `send_comment`, `browse_comments`, etc.\n4. **Back to list**: to play another video, call `return_to_video_list` first, then `play_video`.\n- **Navigation**: tools like `navigate_to_home` can often be used at any time." } - parameters: [] - advice: true - }, - { - name: search_video - description: { zh: "在B站搜索视频内容", en: "Search videos on Bilibili." } - parameters: [ - { - name: keyword - description: { zh: "搜索关键词", en: "Search keyword." } - type: string - required: true - }, - { - name: filter_type - description: { zh: "搜索结果过滤类型:comprehensive(综合)、new(最新)、hot(最多播放)、danmaku(最多弹幕)", en: "Result filter: comprehensive, new, hot (most played), danmaku (most danmaku)." } - type: string - required: false - } - ] - }, - { - name: play_video - description: { zh: "播放指定的视频,可以通过标题或位置选择", en: "Play a specific video by title keyword or by index." } - parameters: [ - { - name: video_title - description: { zh: "要播放的视频标题关键词", en: "Keyword to match the video title." } - type: string - required: false - }, - { - name: video_index - description: { zh: "要播放的视频在搜索结果中的索引位置(从1开始)", en: "Index of the video in the search results (1-based)." } - type: number - required: false - } - ] - }, - { - name: return_to_video_list - description: { zh: "从视频播放界面返回到视频列表界面", en: "Return from the video player page back to the video list." } - parameters: [] - }, - { - name: send_comment - description: { zh: "在当前视频下发送评论", en: "Post a comment under the current video." } - parameters: [ - { - name: comment_text - description: { zh: "要发送的评论内容", en: "Comment text to send." } - type: string - required: true - } - ] - }, - { - name: like_video - description: { zh: "给当前视频点赞", en: "Like the current video." } - parameters: [] - }, - { - name: collect_video - description: { zh: "收藏当前视频", en: "Save (favorite) the current video." } - parameters: [ - { - name: folder_name - description: { zh: "收藏夹名称,留空则使用默认收藏夹", en: "Favorite folder name (leave empty to use the default folder)." } - type: string - required: false - } - ] - }, - { - name: follow_uploader - description: { zh: "关注当前视频的UP主", en: "Follow the uploader (UP) of the current video." } - parameters: [] - }, - { - name: get_video_info - description: { zh: "获取当前视频的详细信息,包括标题、UP主、播放量、评论数等", en: "Get detailed info of the current video (title, uploader, views, comments, etc.)." } - parameters: [] - }, - { - name: browse_comments - description: { zh: "浏览当前视频的评论,获取热门评论内容", en: "Browse comments of the current video and retrieve popular comments." } - parameters: [ - { - name: comment_count - description: { zh: "获取的评论数量,默认为5条", en: "Number of comments to fetch (default: 5)." } - type: number - required: false - } - ] - }, - { - name: navigate_to_home - description: { zh: "导航到B站首页", en: "Navigate to the Bilibili home page." } - parameters: [] - }, - { - name: navigate_to_following - description: { zh: "导航到关注页面,查看关注的UP主动态", en: "Navigate to the Following page to view updates from followed uploaders." } - parameters: [] - }, - { - name: navigate_to_history - description: { zh: "导航到观看历史页面", en: "Navigate to the watch history page." } - parameters: [] - }, - { - name: toggle_fullscreen - description: { zh: "切换视频全屏/非全屏状态", en: "Toggle fullscreen / windowed mode." } - parameters: [] - }, - { - name: adjust_playback_speed - description: { zh: "调整视频播放速度", en: "Adjust video playback speed." } - parameters: [ - { - name: speed - description: { zh: "播放速度:0.5x, 0.75x, 1.0x, 1.25x, 1.5x, 2.0x", en: "Playback speed: 0.5x, 0.75x, 1.0x, 1.25x, 1.5x, 2.0x." } - type: string - required: true - } - ] - } - ] -}*/ - -interface Array { - at(index: number): T | undefined; -} - -const BilibiliAssistant = (function () { - - // 添加 Array.prototype.at 支持 - Array.prototype.at = function (index: number) { - if (index < 0) { - index = this.length + index; - } - return this[index]; - } - - // B站应用包名和主要Activity - const BILIBILI_PACKAGE = "tv.danmaku.bili"; - const MAIN_ACTIVITY = "tv.danmaku.bili.MainActivityV2"; - const VIDEO_ACTIVITY = "com.bilibili.ship.theseus.detail.UnitedBizDetailsActivity"; - - // Helper to create response objects - function createResponse(success: boolean, message: string, data: object | string = {}) { - if (typeof data === 'string') { - return { success: true, message: message, data: data }; - } - return { success, message, ...data }; - } - - // Helper to find a UI element and click it - async function findAndClick(finder: () => Promise): Promise { - const element = await finder(); - if (element) { - await element.click(); - await Tools.System.sleep(1000); - return true; - } - return false; - } - - // Helper to find a UI element by text and click it - async function findAndClickByText(text: string): Promise { - return findAndClick(async () => (await UINode.getCurrentPage()).findByText(text)); - } - - async function ensureMain(packageName: string = BILIBILI_PACKAGE): Promise { - let pageInfo = await Tools.UI.getPageInfo(); - - // 1. Check if we are already on the main activity. - if (pageInfo.packageName === packageName && pageInfo.activityName?.includes(MAIN_ACTIVITY)) { - console.log("Already on the main activity."); - return true; - } - - // 2. If not in the correct app, start it. - if (pageInfo.packageName !== packageName) { - console.log(`Not in the correct app. Current: ${pageInfo.packageName}. Starting ${packageName}.`); - await Tools.System.startApp(packageName); - await Tools.System.sleep(200); // Wait for app to load - } - - // 3. Try pressing back up to 4 times to return to main. - console.log("Attempting to go back to main activity."); - for (let i = 0; i < 4; i++) { - pageInfo = await Tools.UI.getPageInfo(); - if (pageInfo.packageName === packageName && pageInfo.activityName?.includes(MAIN_ACTIVITY)) { - console.log(`Successfully returned to main activity on attempt ${i + 1}.`); - return true; - } - // If we are not in the app anymore, stop trying to go back. - if (pageInfo.packageName !== packageName) { - console.log("Exited the app while trying to go back."); - break; - } - await Tools.UI.pressKey("KEYCODE_BACK"); - await Tools.System.sleep(500); - } - - // 4. If pressing back failed, perform a full restart. - console.log("Pressing back failed or exited app. Performing a full app restart."); - await Tools.System.stopApp(packageName); - await Tools.System.sleep(1000); - await Tools.System.startApp(packageName); - await Tools.System.sleep(4000); - - pageInfo = await Tools.UI.getPageInfo(); - if (pageInfo.packageName === packageName && pageInfo.activityName?.includes(MAIN_ACTIVITY)) { - console.log("Successfully reached main activity after restart."); - return true; - } - - console.error(`Failed to navigate to the main activity of ${packageName}.`); - return false; - } - - async function navigateToSearch(): Promise { - console.log("导航到搜索页面"); - // 确保在主页面 - if (!await ensureMain()) { - return false; - } - - // 点击搜索框容器(正确的搜索区域) - const page = await UINode.getCurrentPage(); - const searchContainer = page.findById('expand_search'); - if (searchContainer) { - await searchContainer.click(); - await Tools.System.sleep(1000); - return true; - } - - return false; - } - - - // --- Bilibili Automation Logic --- - let lastSearchResults: any[] = []; - - /** - * Searches for videos on Bilibili. - * @param params - The search parameters. - * @param {string} params.keyword - The keyword to search for. - * @param {string} [params.filter_type] - The filter to apply (e.g., 'new', 'hot', 'danmaku'). - * @param {number} [params.count=20] - The desired number of video results to return. - * @returns {Promise} A promise that resolves with the search results. - */ - async function search_video(params: { keyword: string; filter_type?: string; count?: number }): Promise { - const { keyword, filter_type, count = 20 } = params; // Default count to 20 - - console.log(`搜索视频: ${keyword}, 过滤类型: ${filter_type}`); - - // 导航到搜索页面 - if (!await navigateToSearch()) { - return createResponse(false, "无法进入搜索页面", { keyword: keyword }); - } - - await Tools.System.sleep(1000); - - // 输入搜索关键词 - const searchInput = (await UINode.getCurrentPage()).findById('search_plate'); - if (searchInput) { - await Tools.UI.setText(keyword); - await Tools.System.sleep(500); - } else { - // 如果没找到输入框,直接输入文本 - await Tools.UI.setText(keyword); - await Tools.System.sleep(500); - } - - // 点击搜索按钮 - const searchButton = (await UINode.getCurrentPage()).findById('action_search'); - if (searchButton) { - await searchButton.click(); - } else { - // 尝试按回车键搜索 - await Tools.UI.pressKey("KEYCODE_ENTER"); - } - - await Tools.System.sleep(4000); // 等待搜索结果加载 - - // Apply filter if specified - if (filter_type) { - await applySearchFilter(filter_type); - } - - // 获取搜索结果 - const results = await getSearchResults(count); - lastSearchResults = results; // Cache the results - - return createResponse(true, `搜索到 ${results.length} 条有效视频结果`, { - keyword: keyword, - filter_type: filter_type, - results: results.map(({ element, ...rest }) => rest), // Exclude element from response - result_count: results.length - }); - } - - async function applySearchFilter(filterType: string): Promise { - const page = await UINode.getCurrentPage(); - const filterButton = page.findByText("综合"); // Default to comprehensive - if (filterButton) { - await filterButton.click(); - await Tools.System.sleep(1000); - } - - if (filterType === "new") { - await findAndClickByText("最新"); - await Tools.System.sleep(1000); - } else if (filterType === "hot") { - await findAndClickByText("最多播放"); - await Tools.System.sleep(1000); - } else if (filterType === "danmaku") { - await findAndClickByText("最多弹幕"); - await Tools.System.sleep(1000); - } - } - - async function getSearchResults(desiredCount: number = 20): Promise { - let page = await UINode.getCurrentPage(); - const results: any[] = []; - const seenTitles = new Set(); - - const resultsContainer = page.findById('tv.danmaku.bili:id/recycler_view'); - if (!resultsContainer) { - console.log("Could not find search results container."); - return results; - } - - // 不再滚动到顶部,直接从当前位置开始处理,避免触发下拉刷新 - let lastResultCount = -1; - const maxScrolls = 10; // Safety break to prevent infinite loops - let scrollCount = 0; - - for (let i = 0; i < maxScrolls; i++) { - page = await UINode.getCurrentPage(); - const currentContainer = page.findById('tv.danmaku.bili:id/recycler_view'); - if (!currentContainer) break; - - for (const item of currentContainer.children) { - const titleNode = item.findById('tv.danmaku.bili:id/title'); - const title = titleNode ? titleNode.text : null; - - if (!title || seenTitles.has(title)) { - continue; // Skip if no title or already seen - } - - // Filtering logic - const isAd = item.findById('tv.danmaku.bili:id/ad_tint_frame') || item.findById('tv.danmaku.bili:id/ad_tag'); - const isUser = item.findById('tv.danmaku.bili:id/user_info'); - const isGame = item.findById('tv.danmaku.bili:id/iv_background'); - const isLive = item.findByText("直播"); - - if (isAd || isUser || isGame || isLive) { - continue; - } - - seenTitles.add(title); - - const uploaderNode = item.findById('tv.danmaku.bili:id/upper') || item.findById('tv.danmaku.bili:id/upuser'); - const playCountNode = item.findById('tv.danmaku.bili:id/play_num'); - const durationNode = item.findById('tv.danmaku.bili:id/duration'); - const postTimeNode = item.findById('tv.danmaku.bili:id/post_time') || item.findById('tv.danmaku.bili:id/danmakus_num'); - - results.push({ - index: results.length + 1, - title: title, - uploader: uploaderNode ? uploaderNode.text : "N/A", - play_count: playCountNode ? playCountNode.text : "N/A", - duration: durationNode ? durationNode.text : "N/A", - post_time: postTimeNode ? postTimeNode.text : "N/A", - element: item - }); - - if (results.length >= desiredCount) { - break; - } - } - - if (results.length >= desiredCount) { - console.log(`Reached desired count of ${desiredCount}.`); - break; - } - - // Check if scrolling is producing new results - if (results.length === lastResultCount) { - console.log("Scrolling is not yielding new results. Stopping."); - break; - } - lastResultCount = results.length; - - // Scroll down to load more - await Tools.UI.swipe(540, 1500, 540, 800); // Swipe up to scroll down - await Tools.System.sleep(1500); // Wait for new items to load - scrollCount++; - } - - console.log(`Scrolling back up ${scrollCount} times.`); - for (let i = 0; i < scrollCount; i++) { - await Tools.UI.swipe(540, 800, 540, 1550); - await Tools.System.sleep(1000); - } - - return results.slice(0, desiredCount); - } - - async function play_video(params: { video_title?: string, video_index?: number }): Promise { - const { video_title, video_index } = params; - - if (!video_title && !video_index) { - return createResponse(false, "请提供视频标题或索引。"); - } - - console.log(`尝试播放视频 - 标题: "${video_title}", 索引: ${video_index}`); - - let page = await UINode.getCurrentPage(); - const seenTitles = new Set(); - let currentVideoCount = 0; - const maxScrolls = 10; - let videoFoundAndClicked = false; - let foundVideoTitle: string | undefined = undefined; - - // Ensure we are on search results page by checking for the recycler view - const initialContainer = page.findById('tv.danmaku.bili:id/recycler_view'); - if (!initialContainer) { - return createResponse(false, "似乎不在搜索结果页面,无法播放视频。"); - } - - // 不再滚动到顶部,直接从当前位置开始查找,避免触发下拉刷新 - let lastSeenCount = -1; - let scrollCount = 0; // 记录向下滚动的次数 - - for (let i = 0; i < maxScrolls; i++) { - page = await UINode.getCurrentPage(); - const currentContainer = page.findById('tv.danmaku.bili:id/recycler_view'); - if (!currentContainer) { - console.log("Could not find search results container during scroll."); - break; - } - - const visibleItems = currentContainer.children; - - for (const item of visibleItems) { - const titleNode = item.findById('tv.danmaku.bili:id/title'); - const title = titleNode ? titleNode.text : null; - - if (!title || seenTitles.has(title)) { - continue; - } - - // Filtering logic - const isAd = item.findById('tv.danmaku.bili:id/ad_tint_frame') || item.findById('tv.danmaku.bili:id/ad_tag'); - const isUser = item.findById('tv.danmaku.bili:id/user_info'); - const isGame = item.findById('tv.danmaku.bili:id/iv_background'); - const isLive = item.findByText("直播"); - - if (isAd || isUser || isGame || isLive) { - continue; - } - - // This is a valid video, add to seen and increment count - seenTitles.add(title); - currentVideoCount++; - - let shouldClick = false; - if (video_index) { - if (currentVideoCount === video_index) { - shouldClick = true; - } - } else if (video_title) { - if (title.includes(video_title)) { - shouldClick = true; - } - } - - if (shouldClick) { - // Try clicking the cover image first, as it's a more reliable target - const coverImage = item.findById('tv.danmaku.bili:id/cover'); - if (coverImage) { - console.log("Clicking on video cover image."); - await coverImage.click(); - } else { - console.log("Cover image not found, clicking on the whole item."); - await item.click(); - } - await Tools.System.sleep(4000); // Increased wait time for video to load - videoFoundAndClicked = true; - foundVideoTitle = title; - break; - } - } - - if (videoFoundAndClicked) { - break; - } - - // Check if we should stop scrolling - if (seenTitles.size === lastSeenCount) { - console.log("Scrolling is not yielding new results. Stopping."); - break; - } - lastSeenCount = seenTitles.size; - - // If we are looking for an index and have already passed it, stop. - if (video_index && currentVideoCount >= video_index) { - break; - } - - // Scroll down to load more - await Tools.UI.swipe(540, 1500, 540, 800); - await Tools.System.sleep(1500); - scrollCount++; // 记录滚动次数 - } - - // 如果没找到视频,滚动回去 - if (!videoFoundAndClicked && scrollCount > 0) { - console.log(`未找到视频,滚动回去 ${scrollCount} 次`); - for (let i = 0; i < scrollCount; i++) { - await Tools.UI.swipe(540, 800, 540, 1550); - await Tools.System.sleep(1000); - } - } - - if (videoFoundAndClicked) { - return createResponse(true, `成功开始播放视频: "${foundVideoTitle}"`, { video_title, video_index }); - } else { - return createResponse(false, "未找到指定的视频", { video_title, video_index }); - } - } - - async function send_comment(params: { comment_text: string }): Promise { - const commentText = params.comment_text || ""; - - if (!await switch_to_comment_activity()) { - return createResponse(false, "不在视频页面"); - } - - console.log(`发送评论: ${commentText}`); - - const page = await UINode.getCurrentPage(); - // 查找评论区域 - let commentArea = page.findById("input"); - if (commentArea) { - await commentArea.click(); - await Tools.System.sleep(1000); - - // 输入评论内容 - await Tools.UI.setText(commentText); - await Tools.System.sleep(500); - - // 查找发送按钮 - const sendButton = page.findByText("发送"); - if (sendButton) { - await sendButton.click(); - await Tools.System.sleep(1000); - - return createResponse(true, "评论发送成功", { comment_text: commentText }); - } - } else { - return createResponse(false, "未找到评论输入框", { comment_text: commentText }); - } - } - async function is_in_video_activity(): Promise { - const pageInfo = await Tools.UI.getPageInfo(); - return pageInfo.activityName?.includes(VIDEO_ACTIVITY); - } - - async function is_on_search_results_page(): Promise { - // A reliable indicator of the search results page is the presence of filter buttons. - const page = await UINode.getCurrentPage(); - const filterButton = page.findByText("综合") && page.findByText("搜索"); // "Comprehensive" filter - return !!filterButton; - } - - async function return_to_video_list(params: {}): Promise { - console.log("Attempting to return to video list..."); - if (await is_in_video_activity()) { - await Tools.UI.pressKey("KEYCODE_BACK"); - await Tools.System.sleep(2000); // Wait for transition - // Verify we are back on the search results page - if (await is_on_search_results_page()) { - return createResponse(true, "Successfully returned to video list."); - } else { - return createResponse(false, "Failed to return to video list, did not land on search results page."); - } - } else { - // If we are already on the search results page, consider it a success. - if (await is_on_search_results_page()) { - return createResponse(true, "Already on the video list page."); - } - return createResponse(false, "Not in a video activity, no action taken."); - } - } - - async function switch_to_comment_activity() { - if (!await is_in_video_activity()) { - return false; - } - console.log("切换到评论页面"); - await findAndClickByText("评论"); - await Tools.System.sleep(1000); - return true; - } - - async function like_video(params: {}): Promise { - if (!await is_in_video_activity()) { - return createResponse(false, "不在视频页面"); - } - console.log("给视频点赞"); - - if (await findAndClick(async () => (await UINode.getCurrentPage()).findById("frame_like"))) { - return createResponse(true, "点赞成功"); - } else { - return createResponse(false, "未找到点赞按钮"); - } - } - - async function collect_video(params: { folder_name?: string }): Promise { - if (!await is_in_video_activity()) { - return createResponse(false, "不在视频页面"); - } - const { folder_name: folderName } = params; - console.log(`开始收藏视频, 文件夹: ${folderName}`); - - if (await findAndClick(async () => (await UINode.getCurrentPage()).findById("frame_fav"))) { - return createResponse(true, "收藏成功", { folder_name: folderName }); - } else { - return createResponse(false, "未找到收藏按钮", { folder_name: folderName }); - } - } - - async function follow_uploader(params: {}): Promise { - if (!await is_in_video_activity()) { - return createResponse(false, "不在视频页面"); - } - console.log("开始关注UP主"); - - if (await findAndClick(async () => (await UINode.getCurrentPage()).findAllById("follow"))) { - return createResponse(true, "关注成功"); - } else { - return createResponse(false, "未找到关注按钮或已经关注"); - } - } - - async function get_video_info(params: {}): Promise { - if (!await is_in_video_activity()) { - return createResponse(false, "不在视频页面"); - } - - console.log("获取视频信息"); - - const page = await UINode.getCurrentPage(); - const videoInfo = { - title: "", - uploader: "", - play_count: "", - comment_count: "", - like_count: "", - coin_count: "", - collect_count: "", - forward_count: "" - }; - - // 获取视频标题 - const titleElement = page.findById('title'); - if (titleElement && titleElement.text) { - videoInfo.title = titleElement.text; - } - // 获取视频点赞数量 - const likeElement = page.findById('frame_like'); - if (likeElement && likeElement.contentDesc) { - videoInfo.like_count = likeElement.contentDesc; - } - // 获取视频收藏数量 - const collectElement = page.findById('frame_fav'); - if (collectElement && collectElement.contentDesc) { - videoInfo.collect_count = collectElement.contentDesc; - } - // 获取视频投币数量 - const coinElement = page.findById('frame_coin'); - if (coinElement && coinElement.contentDesc) { - videoInfo.coin_count = coinElement.contentDesc; - } - - //获取视频转发数量 - const forwardElement = page.findById('frame_share'); - if (forwardElement && forwardElement.contentDesc) { - videoInfo.forward_count = forwardElement.contentDesc; - } - - // 获取UP主名称 - const uploaderElements = page.findAllById("author_name"); - if (uploaderElements.length > 0) { - videoInfo.uploader = uploaderElements.map(u => u.text).join("/"); - } - - - return createResponse(true, "获取视频信息成功", { video_info: videoInfo }); - } - - async function browse_comments(params: { comment_count?: number }): Promise { - const commentCount = params.comment_count || 5; - if (!await switch_to_comment_activity()) { - return createResponse(false, "不在视频页面"); - } - - console.log(`浏览评论,获取${commentCount}条`); - - const page = await UINode.getCurrentPage(); - - // 滚动到评论区域 - await Tools.UI.swipe(540, 1500, 540, 800); - await Tools.System.sleep(2000); - - // 查找评论列表 - const commentList = page.allTexts(); - - return createResponse(true, "获取评论成功", { "all_texts": commentList }); - } - - async function navigate_to_home(params: {}): Promise { - console.log("导航到首页"); - - if (await ensureMain()) { - return createResponse(true, "已导航到首页"); - } else { - return createResponse(false, "无法导航到首页"); - } - } - - async function navigate_to_following(params: {}): Promise { - console.log("导航到关注页面"); - - const page = await UINode.getCurrentPage(); - - const followingTab = page.findByText("关注"); - if (followingTab) { - await followingTab.click(); - await Tools.System.sleep(2000); - - return createResponse(true, "已导航到关注页面"); - } else { - return createResponse(false, "未找到关注标签"); - } - } - - async function navigate_to_history(params: {}): Promise { - console.log("导航到历史页面"); - - const page = await UINode.getCurrentPage(); - - // 查找历史按钮(通常在个人中心或侧边栏) - let historyButton = page.findByText("历史"); - if (!historyButton) { - historyButton = page.findByText("观看历史"); - } - - if (historyButton) { - await historyButton.click(); - await Tools.System.sleep(2000); - - return createResponse(true, "已导航到历史页面"); - } else { - return createResponse(false, "未找到历史按钮"); - } - } - - async function toggle_fullscreen(params: {}): Promise { - console.log("切换全屏状态"); - - // 点击视频中央来显示控制栏 - await Tools.UI.tap(540, 960); - await Tools.System.sleep(1000); - - const page = await UINode.getCurrentPage(); - - // 查找全屏按钮 - let fullscreenButton = page.findByContentDesc("全屏"); - if (!fullscreenButton) { - fullscreenButton = page.findByText("全屏"); - } - if (!fullscreenButton) { - fullscreenButton = page.findById('fullscreen_button'); - } - - if (fullscreenButton) { - await fullscreenButton.click(); - await Tools.System.sleep(1000); - - return createResponse(true, "全屏状态切换成功"); - } else { - return createResponse(false, "未找到全屏按钮"); - } - } - - async function adjust_playback_speed(params: { speed: string }): Promise { - const speed = params.speed || "1.0x"; - - console.log(`调整播放速度为: ${speed}`); - - // 点击视频中央来显示控制栏 - await Tools.UI.tap(540, 960); - await Tools.System.sleep(1000); - - const page = await UINode.getCurrentPage(); - - // 查找速度设置按钮 - let speedButton = page.findByText("倍速"); - if (!speedButton) { - speedButton = page.findByText("1.0x"); - } - if (!speedButton) { - speedButton = page.findById('speed_button'); - } - - if (speedButton) { - await speedButton.click(); - await Tools.System.sleep(1000); - - // 选择目标速度 - const targetSpeedButton = page.findByText(speed); - if (targetSpeedButton) { - await targetSpeedButton.click(); - await Tools.System.sleep(1000); - - return createResponse(true, `播放速度已调整为 ${speed}`, { speed: speed }); - } else { - return createResponse(false, `未找到 ${speed} 速度选项`, { speed: speed }); - } - } else { - return createResponse(false, "未找到速度设置按钮", { speed: speed }); - } - } - - async function main() { - console.log("=== B站智能助手测试 ==="); - console.log(await search_video({ "keyword": "原神" })); - await play_video({ "video_index": 4 }); - console.log(await get_video_info({})); - console.log(await browse_comments({})); - - } - - async function wrapToolExecution(func: (params: any) => Promise, params: any) { - try { - const result = await func(params); - complete({ - ...result, // contains success, message, and other data - }); - } catch (error) { - // This catch is for unexpected errors in tool logic, not for "soft" failures. - console.error(`Tool ${func.name} failed unexpectedly`, error); - complete({ - success: false, - message: `工具执行时发生意外错误: ${error.message}`, - }); - } - } - - return { - main: main, - search_video: async (params) => await wrapToolExecution(search_video, params), - play_video: async (params) => await wrapToolExecution(play_video, params), - return_to_video_list: async (params) => await wrapToolExecution(return_to_video_list, params), - send_comment: async (params) => await wrapToolExecution(send_comment, params), - like_video: async (params) => await wrapToolExecution(like_video, params), - collect_video: async (params) => await wrapToolExecution(collect_video, params), - follow_uploader: async (params) => await wrapToolExecution(follow_uploader, params), - get_video_info: async (params) => await wrapToolExecution(get_video_info, params), - browse_comments: async (params) => await wrapToolExecution(browse_comments, params), - navigate_to_home: async (params) => await wrapToolExecution(navigate_to_home, params), - navigate_to_following: async (params) => await wrapToolExecution(navigate_to_following, params), - navigate_to_history: async (params) => await wrapToolExecution(navigate_to_history, params), - toggle_fullscreen: async (params) => await wrapToolExecution(toggle_fullscreen, params), - adjust_playback_speed: async (params) => await wrapToolExecution(adjust_playback_speed, params) - }; -})(); - -// 逐个导出 -exports.search_video = BilibiliAssistant.search_video; -exports.play_video = BilibiliAssistant.play_video; -exports.return_to_video_list = BilibiliAssistant.return_to_video_list; -exports.send_comment = BilibiliAssistant.send_comment; -exports.like_video = BilibiliAssistant.like_video; -exports.collect_video = BilibiliAssistant.collect_video; -exports.follow_uploader = BilibiliAssistant.follow_uploader; -exports.get_video_info = BilibiliAssistant.get_video_info; -exports.browse_comments = BilibiliAssistant.browse_comments; -exports.navigate_to_home = BilibiliAssistant.navigate_to_home; -exports.navigate_to_following = BilibiliAssistant.navigate_to_following; -exports.navigate_to_history = BilibiliAssistant.navigate_to_history; -exports.toggle_fullscreen = BilibiliAssistant.toggle_fullscreen; -exports.adjust_playback_speed = BilibiliAssistant.adjust_playback_speed; -exports.main = BilibiliAssistant.main; \ No newline at end of file diff --git a/scripts/automatic_ui_base.ts b/scripts/automatic_ui_base.ts deleted file mode 100644 index 408e180..0000000 --- a/scripts/automatic_ui_base.ts +++ /dev/null @@ -1,358 +0,0 @@ -/* METADATA -{ - "name": "Automatic_ui_base", - - "display_name": { - "zh": "自动化基础工具", - "en": "Automation Base Tools" - }, - "description": { "zh": "提供基本的UI自动化工具,能够按照用户的要求帮助操作设备屏幕(如点击、滑动、输入等)。", "en": "Basic UI automation tools to operate the device screen as requested (tap, swipe, input, etc.)." }, - "category": "Automatic", - "enabledByDefault": true, - "tools": [ - { - "name": "usage_advice", - "description": { "zh": "UI自动化建议:\\n- 元素定位选项:\\n • 列表:使用index参数(例如,“点击索引为2的列表项”)\\n • 文本:使用bounds或partialMatch进行模糊匹配(例如,“点击包含‘登录’文字的按钮”)\\n- 操作链:组合多个操作以完成复杂任务(例如,“获取页面信息,然后点击元素”)\\n- 错误处理:如果操作失败,分析页面信息找出原因,并尝试其他方法。\\n- **组合调用(推荐)**:强烈建议在一次响应中组合调用2~3个真实存在的工具,例如依次调用tap → get_page_info,或 click_element → sleep → get_page_info,一次性输出完整的操作序列。软件会自动按顺序依次执行这些工具调用。", "en": "UI automation advice:\\n- Element targeting options:\\n • Lists: use the index parameter (e.g., tap list item at index 2).\\n • Text: use bounds or partialMatch for fuzzy matching (e.g., tap a button containing the text 'Login').\\n- Action chains: combine multiple actions to complete complex tasks (e.g., get page info, then click an element).\\n- Error handling: if an action fails, inspect the page info to find the cause and try alternative methods.\\n- **Combined calls (recommended)**: strongly recommend combining 2~3 real tools in a single response, e.g. tap → get_page_info, or click_element → sleep → get_page_info. The system will execute these tool calls sequentially." }, - "parameters": [], - "advice": true - }, - { - "name": "app_launch", - "description": { "zh": "根据应用包名直接启动应用。如果未找到该包名对应的应用,则返回当前设备的软件安装列表,供你选择其他应用。", "en": "Launch an app by package name. If not found, returns the installed app list for you to choose from." }, - "parameters": [ - { "name": "package_name", "description": { "zh": "应用包名,例如'com.tencent.mm'", "en": "App package name, e.g. 'com.tencent.mm'." }, "type": "string", "required": true } - ] - }, - { - "name": "get_page_info", - "description": { "zh": "获取当前UI屏幕的信息,包括完整的UI层次结构。", "en": "Get information about the current UI screen, including the full UI hierarchy." }, - "parameters": [ - { "name": "format", "description": { "zh": "格式,可选:'xml'或'json',默认'xml'", "en": "Format: 'xml' or 'json' (default: 'xml')." }, "type": "string", "required": false }, - { "name": "detail", "description": { "zh": "详细程度,可选:'minimal'、'summary'或'full',默认'summary'", "en": "Detail level: 'minimal', 'summary', or 'full' (default: 'summary')." }, "type": "string", "required": false } - ] - }, - { - "name": "get_page_screenshot_image", - "description": { "zh": "获取当前屏幕内容的图片版本(截图),返回保存路径。", "en": "Capture the current screen as an image (screenshot) and return the saved file path." }, - "parameters": [] - }, - { - "name": "tap", - "description": { "zh": "在特定坐标模拟点击。", "en": "Simulate a tap at the specified coordinates." }, - "parameters": [ - { "name": "x", "description": { "zh": "X坐标", "en": "X coordinate." }, "type": "number", "required": true }, - { "name": "y", "description": { "zh": "Y坐标", "en": "Y coordinate." }, "type": "number", "required": true } - ] - }, - { - "name": "double_tap", - "description": { "zh": "在特定坐标模拟双击(快速连续点击两次)。", "en": "Simulate a double tap at the specified coordinates (two quick taps)." }, - "parameters": [ - { "name": "x", "description": { "zh": "X坐标", "en": "X coordinate." }, "type": "number", "required": true }, - { "name": "y", "description": { "zh": "Y坐标", "en": "Y coordinate." }, "type": "number", "required": true } - ] - }, - { - "name": "long_press", - "description": { "zh": "在特定坐标模拟长按操作。适用于呼出上下文菜单、拖拽前的按住等场景。", "en": "Simulate a long press at the specified coordinates. Useful for context menus or starting a drag." }, - "parameters": [ - { "name": "x", "description": { "zh": "X坐标", "en": "X coordinate." }, "type": "number", "required": true }, - { "name": "y", "description": { "zh": "Y坐标", "en": "Y coordinate." }, "type": "number", "required": true } - ] - }, - { - "name": "click_element", - "description": { "zh": "点击由资源ID或类名标识的元素。必须至少提供一个标识参数。", "en": "Click an element identified by resourceId or className. You must provide at least one identifier." }, - "parameters": [ - { "name": "resourceId", "description": { "zh": "元素资源ID", "en": "Element resourceId." }, "type": "string", "required": false }, - { "name": "className", "description": { "zh": "元素类名", "en": "Element class name." }, "type": "string", "required": false }, - { "name": "index", "description": { "zh": "要点击的匹配元素,从0开始计数,默认0", "en": "Index of the matched element to click (0-based, default: 0)." }, "type": "number", "required": false }, - { "name": "partialMatch", "description": { "zh": "是否启用部分匹配,默认false", "en": "Enable partial match (default: false)." }, "type": "boolean", "required": false }, - { "name": "bounds", "description": { "zh": "元素边界,格式为'[left,top][right,bottom]'", "en": "Element bounds in format '[left,top][right,bottom]'." }, "type": "string", "required": false } - ] - }, - { - "name": "set_input_text", - "description": { "zh": "在输入字段中设置文本。", "en": "Set text in the current input field." }, - "parameters": [ - { "name": "text", "description": { "zh": "要输入的文本", "en": "Text to input." }, "type": "string", "required": true } - ] - }, - { - "name": "press_key", - "description": { "zh": "模拟按键。", "en": "Simulate a key press." }, - "parameters": [ - { "name": "key_code", "description": { "zh": "键码,例如'KEYCODE_BACK'、'KEYCODE_HOME'等", "en": "Key code, e.g. 'KEYCODE_BACK', 'KEYCODE_HOME'." }, "type": "string", "required": true } - ] - }, - { - "name": "swipe", - "description": { "zh": "模拟滑动手势。", "en": "Simulate a swipe gesture." }, - "parameters": [ - { "name": "start_x", "description": { "zh": "起始X坐标", "en": "Start X coordinate." }, "type": "number", "required": true }, - { "name": "start_y", "description": { "zh": "起始Y坐标", "en": "Start Y coordinate." }, "type": "number", "required": true }, - { "name": "end_x", "description": { "zh": "结束X坐标", "en": "End X coordinate." }, "type": "number", "required": true }, - { "name": "end_y", "description": { "zh": "结束Y坐标", "en": "End Y coordinate." }, "type": "number", "required": true }, - { "name": "duration", "description": { "zh": "持续时间,毫秒,默认300", "en": "Duration in milliseconds (default: 300)." }, "type": "number", "required": false } - ] - } - ] -}*/ - -const UIAutomationTools = (function () { - - interface ToolResponse { - success: boolean; - message: string; - data?: any; - } - - async function get_page_info(params: { format?: 'xml' | 'json', detail?: 'minimal' | 'summary' | 'full' }): Promise { - const result = (await UINode.getCurrentPage()).toFormattedString!(); - return { success: true, message: '成功获取页面信息', data: result }; - } - - async function get_page_screenshot_image(params: {}): Promise { - try { - const screenshotDir = OPERIT_CLEAN_ON_EXIT_DIR; - - // Ensure the directory exists - await Tools.Files.mkdir(screenshotDir, true); - - const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); - const filePath = `${screenshotDir}/ui_screenshot_${timestamp}.png`; - - console.log(`截取当前UI屏幕并保存到: ${filePath}`); - - const result = await Tools.System.shell(`screencap -p ${filePath}`); - - const imageLink = NativeInterface.registerImageFromPath(filePath); - - return { - success: true, - message: `截图已保存到 ${filePath}`, - data: { - file_path: filePath, - image_link: imageLink, - raw_result: result, - }, - }; - } catch (error: any) { - console.error(`获取屏幕截图失败: ${error.message}`); - return { - success: false, - message: `获取屏幕截图失败: ${error.message}`, - }; - } - } - - async function tap(params: { x: number, y: number }): Promise { - const result = await Tools.UI.tap(params.x, params.y); - return { success: true, message: '点击操作成功', data: result }; - } - - async function double_tap(params: { x: number, y: number }): Promise { - const first = await Tools.UI.tap(params.x, params.y); - await Tools.System.sleep(120); - const second = await Tools.UI.tap(params.x, params.y); - return { - success: true, - message: '双击操作成功', - data: { first, second }, - }; - } - - async function long_press(params: { x: number, y: number }): Promise { - const result = await Tools.UI.longPress(params.x, params.y); - return { success: true, message: '长按操作成功', data: result }; - } - - async function click_element(params: { resourceId?: string, className?: string, index?: number, partialMatch?: boolean, bounds?: string }): Promise { - const result = await Tools.UI.clickElement(params); - return { success: true, message: '点击元素操作成功', data: result }; - } - - async function set_input_text(params: { text: string }): Promise { - const result = await Tools.UI.setText(params.text); - return { success: true, message: '输入文本操作成功', data: result }; - } - - async function press_key(params: { key_code: string }): Promise { - const result = await Tools.UI.pressKey(params.key_code); - return { success: true, message: '按键操作成功', data: result }; - } - - async function swipe(params: { start_x: number, start_y: number, end_x: number, end_y: number, duration?: number }): Promise { - const result = await Tools.UI.swipe(params.start_x, params.start_y, params.end_x, params.end_y); - return { success: true, message: '滑动操作成功', data: result }; - } - - async function app_launch(params: { package_name: string }): Promise { - if (!params.package_name) { - return { success: false, message: '必须提供package_name参数' }; - } - - try { - const startResult = await Tools.System.startApp(params.package_name); - - if (startResult && startResult.success) { - return { - success: true, - message: '应用启动成功', - data: { - operation: startResult, - }, - }; - } - - const appList = await Tools.System.listApps(false); - return { - success: false, - message: '未能启动应用,可能未安装或无法找到启动入口。已返回当前安装的应用列表。', - data: { - operation: startResult, - installed_apps: appList, - }, - }; - } catch (error: any) { - console.error(`app_launch 执行失败: ${error.message}`); - try { - const appList = await Tools.System.listApps(false); - return { - success: false, - message: `启动应用时发生错误: ${error.message}。已返回当前安装的应用列表。`, - data: { - installed_apps: appList, - }, - }; - } catch (listError: any) { - console.error(`获取应用列表失败: ${listError.message}`); - return { - success: false, - message: `启动应用失败且无法获取应用列表: ${listError.message}`, - }; - } - } - } - - async function wrapToolExecution

(func: (params: P) => Promise, params: P) { - try { - const result = await func(params); - complete(result); - } catch (error: any) { - console.error(`Tool ${func.name} failed unexpectedly`, error); - complete({ - success: false, - message: `工具执行时发生意外错误: ${error.message}`, - }); - } - } - - async function main() { - console.log("=== UI Automation Tools 测试开始 ===\n"); - const results: any[] = []; - - try { - // 1. 测试 get_page_info - console.log("1. 测试 get_page_info..."); - const pageInfoResult = await get_page_info({}); - results.push({ tool: 'get_page_info', result: pageInfoResult }); - console.log("✓ get_page_info 测试完成\n"); - - // 2. 测试 tap (点击屏幕中心位置) - console.log("2. 测试 tap..."); - const tapResult = await tap({ x: 500, y: 1000 }); - results.push({ tool: 'tap', result: tapResult }); - console.log("✓ tap 测试完成\n"); - await Tools.System.sleep(500); - - // 3. 测试 press_key (按音量上键) - console.log("3. 测试 press_key..."); - const pressKeyResult = await press_key({ key_code: 'KEYCODE_VOLUME_UP' }); - results.push({ tool: 'press_key', result: pressKeyResult }); - console.log("✓ press_key 测试完成\n"); - await Tools.System.sleep(500); - - // 4. 测试 set_input_text - console.log("4. 测试 set_input_text..."); - const setTextResult = await set_input_text({ text: 'UI自动化测试文本' }); - results.push({ tool: 'set_input_text', result: setTextResult }); - console.log("✓ set_input_text 测试完成\n"); - await Tools.System.sleep(500); - - // 5. 测试 swipe (向上滑动) - console.log("5. 测试 swipe..."); - const swipeResult = await swipe({ - start_x: 500, - start_y: 1500, - end_x: 500, - end_y: 500, - duration: 300 - }); - results.push({ tool: 'swipe', result: swipeResult }); - console.log("✓ swipe 测试完成\n"); - await Tools.System.sleep(500); - - // 6. 测试 click_element (尝试点击一个常见的元素) - console.log("6. 测试 click_element..."); - try { - const clickResult = await click_element({ - className: 'android.widget.Button', - index: 0 - }); - results.push({ tool: 'click_element', result: clickResult }); - console.log("✓ click_element 测试完成\n"); - } catch (error: any) { - console.log("⚠ click_element 测试失败(这可能是正常的,如果当前页面没有按钮):", error.message, "\n"); - results.push({ tool: 'click_element', result: { success: false, message: error.message } }); - } - - console.log("=== UI Automation Tools 测试完成 ===\n"); - console.log("测试结果汇总:"); - results.forEach((r, i) => { - const status = r.result.success ? '✓' : '✗'; - console.log(`${i + 1}. ${status} ${r.tool}: ${r.result.message}`); - }); - - complete({ - success: true, - message: "所有UI工具测试完成", - data: results - }); - } catch (error: any) { - console.error("测试过程中发生错误:", error); - complete({ - success: false, - message: `测试失败: ${error.message}`, - data: results - }); - } - } - - return { - get_page_info: (params: { format?: 'xml' | 'json', detail?: 'minimal' | 'summary' | 'full' }) => wrapToolExecution(get_page_info, params), - app_launch: (params: { package_name: string }) => wrapToolExecution(app_launch, params), - get_page_screenshot_image: () => wrapToolExecution(get_page_screenshot_image, {}), - tap: (params: { x: number, y: number }) => wrapToolExecution(tap, params), - double_tap: (params: { x: number, y: number }) => wrapToolExecution(double_tap, params), - long_press: (params: { x: number, y: number }) => wrapToolExecution(long_press, params), - click_element: (params: { resourceId?: string, className?: string, index?: number, partialMatch?: boolean, bounds?: string }) => wrapToolExecution(click_element, params), - set_input_text: (params: { text: string }) => wrapToolExecution(set_input_text, params), - press_key: (params: { key_code: string }) => wrapToolExecution(press_key, params), - swipe: (params: { start_x: number, start_y: number, end_x: number, end_y: number, duration?: number }) => wrapToolExecution(swipe, params), - - main, - }; -})(); - -exports.get_page_info = UIAutomationTools.get_page_info; -exports.app_launch = UIAutomationTools.app_launch; -exports.get_page_screenshot_image = UIAutomationTools.get_page_screenshot_image; -exports.tap = UIAutomationTools.tap; -exports.double_tap = UIAutomationTools.double_tap; -exports.long_press = UIAutomationTools.long_press; -exports.click_element = UIAutomationTools.click_element; -exports.set_input_text = UIAutomationTools.set_input_text; -exports.press_key = UIAutomationTools.press_key; -exports.swipe = UIAutomationTools.swipe; -exports.main = UIAutomationTools.main; diff --git a/scripts/automatic_ui_subagent.ts b/scripts/automatic_ui_subagent.ts deleted file mode 100644 index 53c920f..0000000 --- a/scripts/automatic_ui_subagent.ts +++ /dev/null @@ -1,875 +0,0 @@ -/* METADATA -{ - name: "Automatic_ui_subagent" - - display_name: { - zh: "自动化AutoGLM子代理" - en: "Automated AutoGLM Sub-agent" - }description: { - zh: ''' -兼容AutoGLM,提供基于独立UI控制器模型(例如 autoglm-phone-9b)的高层UI自动化子代理工具,用于根据自然语言意图自动规划并执行点击/输入/滑动等一系列界面操作。 -当用户提出需要帮忙完成某个界面操作任务(例如打开应用、搜索内容、在多个页面之间完成一套步骤)时,可以调用本包由子代理自动规划和执行具体步骤。 -''', - en: ''' -Compatible with AutoGLM. Provides a high-level UI automation sub-agent based on an independent UI-controller model (e.g. autoglm-phone-9b). It can plan and execute a sequence of UI actions (tap/type/swipe) from natural-language intent. -When the user asks you to complete a UI task (e.g. open an app, search content, or finish a multi-step workflow across pages), you can call this package and let the sub-agent plan and execute the steps. -''' - } - category: "Automatic" - - tools: [] - - states: [ - { - id: "virtual_display" - condition: "ui.virtual_display" - inheritTools: true - tools: [ - { - name: "usage_advice" - description: { - zh: ''' - UI子代理使用建议: - - - 屏幕选择规则(重要):不传 agent_id 或传 'default' => 主屏幕;传入且不为 'default' => 对应虚拟屏会话(虚拟屏必须可用,否则会失败)。 - - - 会话复用(重要):多次调用尽量复用同一个 agent_id(沿用上一次返回的 data.agentId),保持在同一虚拟屏/同一应用上下文内。 - - 启动前置(非常重要):当你第一次使用某个 agent_id(新建或更换 agent_id)时,intent 开头必须写“启动XXX应用 ...”,让子代理直接执行 Launch,而不是在桌面自己找。 - - 对话无状态(重要):每次调用对子代理都是全新对话,intent 需要自带上下文,建议固定模板: - 当前任务已经完成: ... - 你需要在此基础上进一步完成: ... - 可能用到的信息: ... - - 意图必须自包含(重要):禁止使用“这五个/继续/同上/刚才说的”等指代。 - - 多对象任务必须在“可能用到的信息”里给出清单(按界面顺序),并明确当前要处理哪个(例如第1个酒店)。 - - 若清单未知,本次调用先让子代理从当前页面识别并复述清单,再进行下一步(必要时拆成多次调用)。 - - 对齐推进(重要):不要一次调用里同时做“收集清单 + 处理清单全部对象”。应拆成:先清单,再按 A→B→C 逐项处理。 - - 并行优先(重要):互不依赖的子任务(多平台搜索/多入口确认/同一对象的信息提取分工)优先用 run_subagent_parallel_virtual 并行;并行时每个子代理建议不同 agent_id 避免互相干扰. - - 并行资源约束(重要):并行分支数必须受“可用独立App数量/可用虚拟屏数量”限制。 - - 同一个App/同一个包名,不能同时存在于两个虚拟屏/两个 agent_id 中并行操作(会导致应用状态错乱/坏掉)。 - - 并行分支数不得超过“可同时存在的独立App数量”(例如只有2个独立App可用,就最多2并行)。 - - 并行调用必须传入 target_app_i=目标应用名(每个 intent_i 对应一个 target_app_i),用于冲突检测;所有启用分支的 target_app_i 必须互不相同. - - 第2次/第N次并行(重试或第二轮任务)不得因为“想更快”而擅自提高并行度;应保持同样的并行上限,只重试失败分支,或改为串行. - - 失败与完成(重要):半成功/误判完成不算完成;应继续纠错推进。仅在连续 2-3 次失败仍无法推进时才停止,并明确失败原因与可选替代方案. - - 例子(详细:1并行 + 2串行) - - 例子1(并行:多平台同时找同一酒店的差评要点) - 目标:同时在 大众点评/美团/携程 搜索“XX酒店”,各自提取“近一年差评Top3要点 + 原文引用 + 日期”,最后主Agent合并. - 调用流程: - 1) 主Agent 一次并行: - - 调用 run_subagent_parallel_virtual,给每个子代理一个互不干扰的 agent_id(例如 dp_1 / mt_1 / xc_1)。 - - intent_1(大众点评)示例: - 当前任务已经完成: 无 - 你需要在此基础上进一步完成: 打开大众点评,搜索“XX酒店”,进入酒店详情,进入评价/差评/低分区,提取近一年差评Top3要点,并把每条要点附带1句原文引用+日期. - 可能用到的信息: 目标酒店名=XX酒店;输出格式=1)要点 2)引用 3)日期;如果搜到多个同名酒店,必须先确认地址/商圈与目标一致. - - intent_2(美团)/ intent_3(携程)同理,各自写清 app、路径、输出格式. - 2) 主Agent 汇总:读取并行返回 results,合并三个平台的提取结果. - 3) 只重试失败分支:若 results 里只有美团分支失败,则只对美团再发起一次(不要重跑点评/携程)。 - - 例如再次调用 run_subagent_virtual(或再次 run_subagent_parallel_virtual 但只填 intent_2),并补充纠错信息: - 当前任务已经完成: 上次在美团搜索到酒店列表,但未能进入评价页(可能入口在“点评/评价”Tab)。 - 你需要在此基础上进一步完成: 重新打开美团搜索“XX酒店”,进入正确的酒店详情页,找到“评价/点评”入口并进入差评/低分区,按同样格式输出Top3. - 可能用到的信息: 若页面出现“住客点评/全部评价/差评”多入口,优先选择“全部评价”再筛选“差评/低分”。 - - 例子2(串行:先清单,再按 A→B→C 逐项处理) - 目标:在携程酒店列表页,先列出前5家酒店;然后只处理第1家酒店的差评要点;处理完再处理第2家…… - 调用流程: - 1) 清单阶段(一次 run_subagent_virtual): - - 主Agent 调用 run_subagent_virtual(intent=..., agent_id="ctrip_1") - - intent 示例: - 当前任务已经完成: 无 - 你需要在此基础上进一步完成: 打开携程,搜索“杭州 西湖 酒店”,进入列表页;把当前屏幕能看到的酒店按从上到下顺序列出前5个(名称+价格/评分如可见),并停留在列表页不要进入详情. - 可能用到的信息: 输出必须包含清单序号1-5;若需要滚动才能凑满5个可以滚动一次,但仍要保持顺序. - - 该次返回 data.agentId 记为 A(后续复用) - 2) 单对象阶段:处理“清单第1家”(一次 run_subagent_virtual,复用 agent_id=A) - - intent 示例: - 当前任务已经完成: 已获得酒店清单(1)酒店A 2)酒店B 3)酒店C 4)酒店D 5)酒店E),当前停留在列表页. - 你需要在此基础上进一步完成: 进入酒店A详情页,找到评价页并筛选差评/低分,提取差评Top3要点(每条含1句原文引用)。完成后返回列表页并确认列表顶部仍是酒店A/B/C顺序. - 可能用到的信息: 目标对象=清单第1家=酒店A;如果进入后发现标题不是酒店A则立刻返回列表并重新点击正确条目. - 3) 继续处理第2家/第3家……(每次都复用 agent_id=A,并在 intent 中显式写“已完成/下一步/关键信息”,以及“当前目标=清单第2家=酒店B”)。 - - 例子3(串行:发消息,强调“页面确认+会话复用”) - 目标:在微信给“张三”发送“我到楼下了”,并确认发送成功. - 调用流程: - 1) 打开并定位会话(一次 run_subagent_virtual): - - 主Agent 调用 run_subagent_virtual(intent=..., agent_id="wechat_1") - - intent 示例: - 当前任务已经完成: 无 - 你需要在此基础上进一步完成: 打开微信,进入聊天列表,搜索联系人“张三”,打开与“张三”的聊天页面;必须确认页面顶部标题=张三. - 可能用到的信息: 如果搜索结果有多个“张三”,需要根据头像/备注/地区等二次确认;不确定时返回并说明. - - 该次返回 data.agentId 记为 W(后续复用) - 2) 发送并二次确认(一次 run_subagent_virtual,复用 agent_id=W): - - intent 示例: - 当前任务已经完成: 已打开与“张三”的聊天页,顶部标题=张三. - 你需要在此基础上进一步完成: 输入并发送消息“我到楼下了”;发送后在消息列表中确认该消息出现在最新一条且无明显发送失败标记(如红色感叹号). - 可能用到的信息: 若出现权限弹窗/键盘遮挡,先处理弹窗再继续;若发送失败,尝试重发一次并说明原因. - ''', - en: ''' - UI sub-agent usage advice: - - - Screen selection rule (important): omitted agent_id or 'default' => main screen; provided and not 'default' => the requested virtual display session (must be available, otherwise the run fails). - - - Session reuse (important): reuse the same agent_id across calls when possible (use the returned data.agentId) to stay in the same virtual display and app context. - - Launch first (very important): when you first use an agent_id (new or changed), the intent must start with "Launch ..." so the sub-agent performs a Launch directly instead of searching from the home screen. - - Stateless per call (important): each call is a new conversation; include all necessary context. Recommended template: Completed so far / Next objective / Useful info. - - Self-contained intent (important): do not use references like "those five / continue / same as above". - - For multi-item tasks, list items in "Useful info" (in on-screen order) and specify which one to handle now. - - If the list is unknown, first ask the sub-agent to identify and restate the list from the current screen, then proceed. - - Step-by-step alignment (important): do not combine "collect list" + "process all items" in one call. Split into: list first, then process A→B→C. - - Prefer parallelism (important): for independent subtasks (multi-platform search / multi-entry verification / extraction sub-tasks for the same object), use run_subagent_parallel_virtual. Use different agent_id per branch to avoid interference. - - Parallel resource constraints (important): parallel branches are limited by available independent apps / virtual displays. - - The same app/package must not be operated in parallel across two virtual displays / agent_id (it can break app state). - - Branch count must not exceed available independent apps. - - Parallel calls must provide target_app_i for each enabled intent_i; all target_app_i must be different. - - Do not increase parallelism in later rounds; keep the same upper bound and only retry failed branches, or switch to serial. - - Failure vs done (important): partial success is not done; keep correcting and progressing. Only stop after 2-3 consecutive failures with a clear reason and alternatives. -''' - } - advice: true - parameters: [] - } - - { - name: "run_subagent_main" - description: { - zh: "在主屏幕运行 UI 子代理(强制主屏)。", - en: "Run the UI sub-agent on the main screen (forced main screen)." - } - parameters: [ - { - name: "intent" - description: { - zh: "任务意图描述", - en: "Task intent description" - } - type: "string" - required: true - } - { - name: "target_app" - description: { - zh: "目标应用名/包名(可选)", - en: "Target app name/package (optional)" - } - type: "string" - required: false - } - { - name: "max_steps" - description: { - zh: "最大执行步数(默认20)", - en: "Maximum execution steps (default: 20)" - } - type: "number" - required: false - } - ] - } - - { - name: "run_subagent_virtual" - description: { - zh: "在虚拟屏幕会话运行 UI 子代理(强制虚拟屏)。", - en: "Run the UI sub-agent on a virtual-display session (forced virtual screen)." - } - parameters: [ - { - name: "intent" - description: { - zh: "任务意图描述", - en: "Task intent description" - } - type: "string" - required: true - } - { - name: "target_app" - description: { - zh: "目标应用名/包名(可选)", - en: "Target app name/package (optional)" - } - type: "string" - required: false - } - { - name: "max_steps" - description: { - zh: "最大执行步数(默认20)", - en: "Maximum execution steps (default: 20)" - } - type: "number" - required: false - } - { - name: "agent_id" - description: { - zh: "虚拟屏会话 agent_id(必须为非 'default';可传入复用,或留空复用上次返回的 data.agentId)。", - en: "Virtual-screen session agent_id (must be non-'default'; pass to reuse, or omit to reuse returned data.agentId)." - } - type: "string" - required: false - } - ] - } - - { - name: "run_subagent_parallel_virtual" - description: { - zh: ''' -并行运行 1-4 个 UI 子代理(强制虚拟屏)。 - - 注意:并行调用时,每个子代理对它自身都是全新对话,因此 intent_1..4 需要由主Agent分别写清楚“已完成/下一步/关键信息”。 - 建议并行时每个子代理使用不同的 agent_id(必须显式传入,且不能为 'default'),避免操作同一虚拟屏幕造成冲突。 - 如果并行任务中仅有部分子代理失败,则只对失败的子代理继续发起后续调用(补充纠错信息、提高约束),不要让已成功的子代理重复执行。 - 每个 intent_i 必须自包含;建议不同 agent_id;只重试失败的 intent_i。 - 典型场景:多平台并行搜索;同一对象多入口确认/交叉校验;把“同一对象A”的分工并行做完后再进入B. - ''' - } - parameters: [ - { - name: "intent_1" - description: { - zh: "第1个子代理意图(推荐使用:当前任务已经完成/你需要进一步完成/可能用到的信息 三段式)", - en: "Intent for sub-agent #1 (recommended template: Completed so far / Next objective / Useful info)." - } - type: "string" - required: true - } - { - name: "target_app_1" - description: { - zh: "第1个子代理目标应用名(必填,用于并行冲突检测;各分支必须不同)", - en: "Target app name for sub-agent #1 (required for conflict detection; must be different across branches)." - } - type: "string" - required: true - } - { - name: "max_steps_1" - description: { - zh: "第1个子代理最大步数(默认20)", - en: "Max steps for sub-agent #1 (default: 20)." - } - type: "number" - required: false - } - { - name: "agent_id_1" - description: { - zh: "第1个子代理 agent_id(必填,且不能为 'default';用于指定虚拟屏会话;并行建议不同)", - en: "agent_id for sub-agent #1 (required, must not be 'default'; selects a virtual-display session; use different agent_id per branch)." - } - type: "string" - required: true - } - - { - name: "intent_2" - description: { - zh: "第2个子代理意图(可选)", - en: "Intent for sub-agent #2 (optional)." - } - type: "string" - required: false - } - { - name: "target_app_2" - description: { - zh: "第2个子代理目标应用名(当 intent_2 存在时必填;各分支必须不同)", - en: "Target app name for sub-agent #2 (required when intent_2 is provided; must be different across branches)." - } - type: "string" - required: false - } - { - name: "max_steps_2" - description: { - zh: "第2个子代理最大步数(默认20)", - en: "Max steps for sub-agent #2 (default: 20)." - } - type: "number" - required: false - } - { - name: "agent_id_2" - description: { - zh: "第2个子代理 agent_id(当 intent_2 存在时必填,且不能为 'default';用于指定虚拟屏会话;并行建议不同)", - en: "agent_id for sub-agent #2 (required when intent_2 is provided, must not be 'default'; selects a virtual-display session; use different agent_id per branch)." - } - type: "string" - required: false - } - - { - name: "intent_3" - description: { - zh: "第3个子代理意图(可选)", - en: "Intent for sub-agent #3 (optional)." - } - type: "string" - required: false - } - { - name: "target_app_3" - description: { - zh: "第3个子代理目标应用名(当 intent_3 存在时必填;各分支必须不同)", - en: "Target app name for sub-agent #3 (required when intent_3 is provided; must be different across branches)." - } - type: "string" - required: false - } - { - name: "max_steps_3" - description: { - zh: "第3个子代理最大步数(默认20)", - en: "Max steps for sub-agent #3 (default: 20)." - } - type: "number" - required: false - } - { - name: "agent_id_3" - description: { - zh: "第3个子代理 agent_id(当 intent_3 存在时必填,且不能为 'default')", - en: "agent_id for sub-agent #3 (required when intent_3 is provided; must not be 'default')." - } - type: "string" - required: false - } - - { - name: "intent_4" - description: { - zh: "第4个子代理意图(可选)", - en: "Intent for sub-agent #4 (optional)." - } - type: "string" - required: false - } - { - name: "target_app_4" - description: { - zh: "第4个子代理目标应用名(当 intent_4 存在时必填;各分支必须不同)", - en: "Target app name for sub-agent #4 (required when intent_4 is provided; must be different across branches)." - } - type: "string" - required: false - } - { - name: "max_steps_4" - description: { - zh: "第4个子代理最大步数(默认20)", - en: "Max steps for sub-agent #4 (default: 20)." - } - type: "number" - required: false - } - { - name: "agent_id_4" - description: { - zh: "第4个子代理 agent_id(当 intent_4 存在时必填,且不能为 'default')", - en: "agent_id for sub-agent #4 (required when intent_4 is provided; must not be 'default')." - } - type: "string" - required: false - } - ] - } - - { - name: "close_all_virtual_displays" - description: { - zh: "关闭所有虚拟屏幕。", - en: "Close all virtual displays." - } - parameters: [] - } - - { - name: "run_subagent_main" - description: { - zh: "在主屏幕运行 UI 子代理(强制主屏)。", - en: "Run the UI sub-agent on the main screen (forced main screen)." - } - parameters: [ - { - name: "intent" - description: { - zh: "任务意图描述", - en: "Task intent description" - } - type: "string" - required: true - } - { - name: "target_app" - description: { - zh: "目标应用名/包名(可选)", - en: "Target app name/package (optional)" - } - type: "string" - required: false - } - { - name: "max_steps" - description: { - zh: "最大执行步数(默认20)", - en: "Maximum execution steps (default: 20)" - } - type: "number" - required: false - } - ] - } - ] - } - - { - id: "main_screen" - condition: "!ui.virtual_display" - inheritTools: true - tools: [ - { - name: "usage_advice" - description: { - zh: ''' - UI子代理使用建议(主屏模式): - - - 屏幕选择规则(重要):主屏模式始终在主屏幕执行;agent_id 会被忽略/不适用。 - - - 启动前置(非常重要):当你第一次需要操作某个应用时,intent 开头必须写“启动XXX应用 ...”,让子代理直接执行 Launch,而不是在桌面自己找。 - - 对话无状态(重要):每次调用对子代理都是全新对话,intent 必须自带上下文,建议固定模板: - 当前任务已经完成: ... - 你需要在此基础上进一步完成: ... - 可能用到的信息: ... - - 意图必须自包含(重要):禁止使用“这五个/继续/同上/刚才说的”等指代. - - 严格串行(重要):主屏模式不支持并行工具;一次只做一个明确子目标,必要时拆成多次调用. - - 不支持会话复用(重要):主屏模式不支持 agent_id,会话复用相关策略不适用. - - 失败与完成(重要):半成功不算完成;应继续纠错推进.仅在连续 2-3 次失败仍无法推进时才停止,并明确失败原因与可选替代方案. - ''', - en: ''' - UI sub-agent usage advice (main-screen mode): - - - Screen selection rule (important): main-screen mode always operates on the main screen; agent_id is ignored / not applicable. - - - Launch first (very important): when you need to operate an app for the first time, the intent must start with "Launch ..." so the sub-agent performs Launch directly. - - Stateless per call (important): each call is a new conversation; include all context. Recommended template: Completed so far / Next objective / Useful info. - - Self-contained intent (important): do not use references like "those five / continue / same as above". - - Strictly serial (important): main-screen mode does not support parallel tools; do one clear sub-goal per call. - - No session reuse (important): main-screen mode does not support agent_id; session reuse strategies do not apply. - - Failure vs done (important): partial success is not done; keep progressing. Only stop after 2-3 consecutive failures with a clear reason and alternatives. -''' - } - advice: true - parameters: [] - } - - { - name: "run_subagent_main" - description: { - zh: ''' - 在主屏幕运行 UI 子代理(强制主屏)。 - - 注意:主屏模式不支持虚拟屏会话与并行工具。 - ''', - en: ''' - Run the UI sub-agent on the main screen (forced main screen). - - Note: main-screen mode does not support virtual sessions and does not support parallel tools. - ''' - } - parameters: [ - { - name: "intent" - description: { - zh: "任务意图描述,例如:'打开微信并发送一条消息' 或 '在B站搜索某个视频'", - en: "Task intent description, e.g. 'Open WeChat and send a message' or 'Search a video on Bilibili'." - } - type: "string" - required: true - } - { - name: "target_app" - description: { - zh: "目标应用名/包名(建议传入,用于在虚拟屏未创建时先执行一次默认 Launch 预热虚拟屏,避免在主屏误操作;约定:当用户要求分析当前屏幕/页面内容时不要传 target_app,也不要预热虚拟屏)", - en: "Target app name/package (recommended). Helps with a default Launch/warm-up and avoids operating on the wrong screen. Convention: when the user asks to analyze the current screen/page, do not pass target_app and do not prewarm the virtual display." - } - type: "string" - required: false - } - { - name: "max_steps" - description: { - zh: "最大执行步数,默认20,可根据任务复杂度调整。", - en: "Maximum execution steps (default: 20). Adjust based on task complexity." - } - type: "number" - required: false - } - ] - } - ] - } - ] - }*/ - -const UIAutomationSubAgentTools = (function () { - - const CACHE_KEY = '__operit_ui_subagent_cached_agent_id'; - function getCachedAgentId(): string | undefined { - try { - return (globalThis as unknown as Record)[CACHE_KEY]; - } catch (_e) { - return undefined; - } - } - function setCachedAgentId(value: unknown) { - try { - if (value === undefined || value === null || String(value).length === 0) { - delete (globalThis as unknown as Record)[CACHE_KEY]; - } else { - (globalThis as unknown as Record)[CACHE_KEY] = String(value); - } - } catch (_e) { - } - } - - interface ToolResponse { - success: boolean; - message: string; - data?: unknown; - } - - function getPackageState(): string | undefined { - try { - return getState(); - } catch (_e) { - return undefined; - } - } - - type InstalledAppEntry = { name: string, pkg?: string, raw: string }; - - function errorMessage(e: unknown): string { - return e instanceof Error ? e.message : String(e); - } - - function getStringArrayFromUnknown(v: unknown): string[] { - if (!Array.isArray(v)) return []; - return v.map((x) => String(x || '').trim()).filter((s) => s.length > 0); - } - - function parseInstalledAppEntry(raw: string): InstalledAppEntry { - const s = String(raw || '').trim(); - if (!s) return { name: '', raw: '' }; - const open = s.lastIndexOf('('); - const close = s.lastIndexOf(')'); - if (open >= 0 && close === s.length - 1 && open < close) { - const name = s.slice(0, open).trim(); - const pkg = s.slice(open + 1, close).trim(); - if (name && pkg) return { name, pkg, raw: s }; - } - return { name: s, raw: s }; - } - - async function getInstalledApps(): Promise<{ entries: InstalledAppEntry[], names: string[] }> { - const appList = await Tools.System.listApps(false) as unknown; - const rawItems = getStringArrayFromUnknown((appList as { packages?: unknown } | null)?.packages); - const entries = rawItems.map(parseInstalledAppEntry).filter((e) => e.name); - const nameMap = new Map(); - for (const e of entries) nameMap.set(e.name.toLowerCase(), e.name); - const names = Array.from(nameMap.values()).sort((a, b) => a.localeCompare(b)); - return { entries, names }; - } - - function matchTarget(targetApp: string, installed: InstalledAppEntry[]): { run: string, key: string } | null { - const t = String(targetApp || '').trim(); - if (!t) return null; - const tl = t.toLowerCase(); - const m = - installed.find((a) => (a.pkg || '').toLowerCase() === tl) || - installed.find((a) => a.name.toLowerCase() === tl) || - installed.find((a) => a.raw.toLowerCase() === tl); - if (!m) return null; - const run = m.pkg || m.name; - return { run, key: run.toLowerCase() }; - } - - async function usage_advice(_params: {}): Promise { - const state = getPackageState(); - const isMainScreen = String(state).toLowerCase() === 'main_screen'; - return { - success: true, - message: 'UI子代理使用建议', - data: { - advice: isMainScreen - ? ("主屏模式:不支持 agent_id,会话复用策略不适用;不支持并行工具,一次只做一个明确子目标(必要时拆多次)。\n" + - "屏幕选择规则:不传 agent_id 或传 'default' => 主屏幕;传入且不为 'default' => 虚拟屏(主屏模式下会忽略并强制主屏)。\n" + - "启动前置(非常重要):当你第一次需要操作某个应用时,intent 开头必须写“启动XXX应用 ...”,让子代理直接执行 Launch,而不是在桌面自己找。\n" + - "对话无状态:每次调用是新对话,intent 写清 已完成/下一步/关键信息。\n" + - "自包含:别用‘这五个/继续/同上’;多对象要么列清单+当前目标,要么先让子代理在当前页识别并复述清单。\n" + - "失败与完成:半成功不算完成;未达成目标继续推进,连续 2-3 次失败再停并说明原因。") - : ("虚拟屏模式:尽量复用 agent_id(沿用 data.agentId)保持同一虚拟屏/同一应用上下文。\n" + - "屏幕选择规则:不传 agent_id 或传 'default' => 主屏幕;传入且不为 'default' => 对应虚拟屏会话(虚拟屏必须可用,否则失败)。为避免误操作主屏,虚拟屏模式下首次调用建议显式传入 agent_id。\n" + - "启动前置(非常重要):当你第一次使用某个 agent_id(新建或更换 agent_id)时,intent 开头必须写“启动XXX应用 ...”,让子代理直接执行 Launch,而不是在桌面自己找。\n" + - "对话无状态:每次调用是新对话,intent 写清 已完成/下一步/关键信息。\n" + - "自包含:别用‘这五个/继续/同上’;多对象要么列清单+当前目标,要么先让子代理在当前页识别并复述清单。\n" + - "对齐+并行:先清单后逐项(A→B→C);独立子任务/同一对象多入口优先并行(run_subagent_parallel_virtual),只重试失败分支。\n" + - "并行资源约束:并行分支数必须受可用独立App/虚拟屏数量限制;同一个App/包名不能同时出现在两个虚拟屏/两个agent_id 中并行操作(会坏)。并行调用必须传 target_app_i=目标应用名,且各分支 target_app_i 不能重复;第2次/第N次并行不得擅自提高并行度,保持上限,只重试失败分支或改串行。\n" + - "失败与完成:半成功不算完成;未达成目标继续推进,连续 2-3 次失败再停并说明原因。" - ), - }, - }; - } - - async function run_subagent_internal(params: { intent: string, max_steps?: number, agent_id?: string, target_app?: string }): Promise { - const { intent, max_steps, agent_id, target_app } = params; - const state = getPackageState(); - const isMainScreen = String(state).toLowerCase() === 'main_screen'; - - const explicitAgentId = (agent_id === undefined || agent_id === null) ? '' : String(agent_id).trim(); - const cachedAgentId = getCachedAgentId(); - - let agentIdToUse: string | undefined; - if (isMainScreen) { - agentIdToUse = 'default'; - } else { - agentIdToUse = explicitAgentId.length > 0 ? explicitAgentId : cachedAgentId; - if (agentIdToUse === undefined || agentIdToUse === null || String(agentIdToUse).trim().length === 0) { - return { - success: false, - message: "虚拟屏模式下未指定 agent_id:为避免误操作主屏幕,请显式传入 agent_id(且不为 'default')来使用虚拟屏会话;或先完成一次成功调用并复用返回的 data.agentId。", - }; - } - } - - let targetAppForRun = target_app; - if (target_app && String(target_app).trim().length > 0) { - const installed = await getInstalledApps(); - const matched = matchTarget(target_app, installed.entries); - if (!matched) { - return { - success: false, - message: `目标应用不存在:当前给定的 target_app=“${String(target_app).trim()}” 未在已安装应用中找到。已返回已安装应用名列表。`, - data: { - target_app: String(target_app).trim(), - installed_apps: installed.names, - }, - }; - } - targetAppForRun = matched.run; - } - - const result = await Tools.UI.runSubAgent(intent, max_steps, agentIdToUse, targetAppForRun) as unknown; - const agentId = (result as { agentId?: unknown } | null)?.agentId; - if (agentId && String(agentId).trim().length > 0 && String(agentId).trim().toLowerCase() !== 'default') { - setCachedAgentId(agentId); - } - return { - success: true, - message: 'UI子代理执行完成', - data: result, - }; - } - - async function run_subagent_main(params: { intent: string, max_steps?: number, target_app?: string }): Promise { - const { intent, max_steps, target_app } = params; - return run_subagent_internal({ intent, max_steps, target_app, agent_id: 'default' }); - } - - async function run_subagent_virtual(params: { intent: string, max_steps?: number, agent_id?: string, target_app?: string }): Promise { - const { intent, max_steps, agent_id, target_app } = params; - - const explicitAgentId = (agent_id === undefined || agent_id === null) ? '' : String(agent_id).trim(); - const cachedAgentId = getCachedAgentId(); - const agentIdToUse = explicitAgentId.length > 0 ? explicitAgentId : cachedAgentId; - - if (agentIdToUse === undefined || agentIdToUse === null || String(agentIdToUse).trim().length === 0 || String(agentIdToUse).trim().toLowerCase() === 'default') { - return { - success: false, - message: "虚拟屏模式必须使用非 'default' 的 agent_id:请显式传入 agent_id(非 'default'),或先完成一次成功调用并复用返回的 data.agentId。", - }; - } - - return run_subagent_internal({ intent, max_steps, target_app, agent_id: String(agentIdToUse) }); - } - - type Slot = 1 | 2 | 3 | 4; - type ParallelParams = { - intent_1: string, target_app_1: string, max_steps_1?: number, agent_id_1?: string, - intent_2?: string, target_app_2?: string, max_steps_2?: number, agent_id_2?: string, - intent_3?: string, target_app_3?: string, max_steps_3?: number, agent_id_3?: string, - intent_4?: string, target_app_4?: string, max_steps_4?: number, agent_id_4?: string, - }; - - async function run_subagent_parallel_internal(params: ParallelParams): Promise { - const state = getPackageState(); - const isMainScreen = String(state).toLowerCase() === 'main_screen'; - if (isMainScreen) { - return { - success: false, - message: "主屏模式不支持 run_subagent_parallel_virtual 并行调用。请改用 run_subagent_main 串行执行。", - }; - } - - const slots: Slot[] = [1, 2, 3, 4]; - type IntentKey = `intent_${Slot}`; - type TargetKey = `target_app_${Slot}`; - type MaxStepsKey = `max_steps_${Slot}`; - type AgentIdKey = `agent_id_${Slot}`; - - const activeSlots = slots - .map((i) => { - const intent = params[`intent_${i}` as IntentKey]; - if (!intent || intent.trim().length === 0) return null; - const targetApp = params[`target_app_${i}` as TargetKey]; - return { index: i, targetApp }; - }) - .filter((x): x is { index: Slot, targetApp: string | undefined } => Boolean(x)); - - const missingTargets = activeSlots - .filter((s) => s.targetApp === undefined || s.targetApp === null || String(s.targetApp).trim().length === 0) - .map((s) => s.index); - if (missingTargets.length > 0) { - return { - success: false, - message: `并行参数错误:intent_${missingTargets.join(', intent_')} 缺少 target_app_${missingTargets.join(', target_app_')}(目标应用名)。并行时必须为每个启用分支传入目标应用名,用于检测“同一应用不能出现在两个虚拟屏/agent_id”的冲突。`, - }; - } - - const missingAgentIds = activeSlots - .filter((s) => { - const v = params[`agent_id_${s.index}` as AgentIdKey]; - const id = v === undefined || v === null ? '' : String(v).trim(); - return id.length === 0 || id.toLowerCase() === 'default'; - }) - .map((s) => s.index); - if (missingAgentIds.length > 0) { - return { - success: false, - message: `并行参数错误:虚拟屏并行模式下,每个启用分支必须显式传入非 'default' 的 agent_id。缺少/非法 agent_id 的分支:${missingAgentIds.map((i) => `#${i}`).join(',')}。`, - }; - } - - const installed = await getInstalledApps(); - const resolvedBySlot = new Map(); - const missingApps: string[] = []; - - for (const s of activeSlots) { - const t = String(s.targetApp || '').trim(); - const m = matchTarget(t, installed.entries); - if (!m) missingApps.push(t); - else resolvedBySlot.set(s.index, m); - } - - if (missingApps.length) { - return { - success: false, - message: `目标应用不存在:当前给定的 target_app 列表中包含未安装/不存在的应用:${missingApps.map((s) => `“${String(s).trim()}”`).join(',')}。已返回已安装应用名列表。`, - data: { - missing_apps: missingApps, - installed_apps: installed.names, - }, - }; - } - - const used: Record = {}; - for (const s of activeSlots) { - const key = resolvedBySlot.get(s.index)?.key || String(s.targetApp).trim().toLowerCase(); - const prev = used[key]; - if (prev !== undefined) { - return { - success: false, - message: `并行参数错误:target_app_${prev} 与 target_app_${s.index} 重复(同一目标应用=“${String(s.targetApp).trim()}”)。同一应用不能同时在两个虚拟屏/agent_id 中并行操作。`, - }; - } - used[key] = s.index; - } - - type ParallelSubAgentResult = - | { index: Slot, success: true, result: unknown } - | { index: Slot, success: false, error: string }; - - const tasks = slots - .map((i) => { - const intent = params[`intent_${i}` as IntentKey]; - if (!intent || intent.trim().length === 0) return null; - - const maxSteps = params[`max_steps_${i}` as MaxStepsKey]; - const agentId = params[`agent_id_${i}` as AgentIdKey]; - const targetApp = resolvedBySlot.get(i)?.run ?? params[`target_app_${i}` as TargetKey]; - - return (async (): Promise => { - try { - const result = await Tools.UI.runSubAgent( - String(intent), - maxSteps === undefined ? undefined : Number(maxSteps), - agentId === undefined || agentId === null || String(agentId).trim().length === 0 ? undefined : String(agentId).trim(), - targetApp - ) as unknown; - return { index: i, success: true, result }; - } catch (e: unknown) { - return { index: i, success: false, error: errorMessage(e) }; - } - })(); - }) - .filter((x): x is Promise => x !== null); - - const results = await Promise.all(tasks); - const okCount = results.filter((r) => r.success).length; - return { - success: true, - message: `并行UI子代理执行完成:成功 ${okCount} 个 / 共 ${results.length} 个`, - data: { - results, - }, - }; - } - - async function run_subagent_parallel_virtual(params: ParallelParams): Promise { - return run_subagent_parallel_internal(params); - } - - async function close_all_virtual_displays(_params: {}): Promise { - const result = await toolCall('close_all_virtual_displays', {}); - const ok = (result as { success?: boolean } | null)?.success !== false; - const error = (result as { error?: unknown } | null)?.error; - return { - success: ok, - message: ok - ? '已关闭所有虚拟屏幕。' - : `关闭虚拟屏幕失败:${error ? String(error) : 'unknown error'}`, - data: result, - }; - } - - async function wrapToolExecution

(func: (params: P) => Promise, params: P) { - try { - const result = await func(params); - complete(result); - } catch (error: unknown) { - console.error(`Tool ${func.name} failed unexpectedly`, error); - complete({ - success: false, - message: `工具执行时发生意外错误: ${errorMessage(error)}`, - }); - } - } - - return { - usage_advice: (params: {}) => wrapToolExecution(usage_advice, params), - run_subagent_main: (params: { intent: string, max_steps?: number, target_app?: string }) => wrapToolExecution(run_subagent_main, params), - run_subagent_virtual: (params: { intent: string, max_steps?: number, agent_id?: string, target_app?: string }) => wrapToolExecution(run_subagent_virtual, params), - close_all_virtual_displays: (params: {}) => wrapToolExecution(close_all_virtual_displays, params), - run_subagent_parallel_virtual: (params: { - intent_1: string, target_app_1: string, max_steps_1?: number, agent_id_1?: string, - intent_2?: string, target_app_2?: string, max_steps_2?: number, agent_id_2?: string, - intent_3?: string, target_app_3?: string, max_steps_3?: number, agent_id_3?: string, - intent_4?: string, target_app_4?: string, max_steps_4?: number, agent_id_4?: string, - }) => wrapToolExecution(run_subagent_parallel_virtual, params), - }; -})(); - -exports.usage_advice = UIAutomationSubAgentTools.usage_advice; -exports.run_subagent_main = UIAutomationSubAgentTools.run_subagent_main; -exports.run_subagent_virtual = UIAutomationSubAgentTools.run_subagent_virtual; -exports.close_all_virtual_displays = UIAutomationSubAgentTools.close_all_virtual_displays; -exports.run_subagent_parallel_virtual = UIAutomationSubAgentTools.run_subagent_parallel_virtual; diff --git a/scripts/automatic_xiaohongshu_assistant.ts b/scripts/automatic_xiaohongshu_assistant.ts deleted file mode 100644 index d8ea5b3..0000000 --- a/scripts/automatic_xiaohongshu_assistant.ts +++ /dev/null @@ -1,1260 +0,0 @@ -/* -METADATA -{ - // 小红书智能助手包 - name: "Automatic_xiaohongshu_assistant", - - display_name: { - zh: "小红书自动化助手" - en: "Xiaohongshu Automation Assistant" - }description: { zh: "高级小红书智能助手,通过UI自动化技术实现小红书应用交互,支持内容浏览、搜索查看、评论互动、内容发布等功能,为AI赋予小红书社交和内容创作能力。适用于内容营销、社交互动、生活分享等场景。", en: "Advanced Xiaohongshu (RED) assistant powered by UI automation. Supports browsing feeds, searching and viewing content, commenting and interactions, and publishing posts, enabling AI-driven social interaction and content creation. Useful for content marketing and social engagement." }, - - // Tools in this package - category: "Automatic" - tools: [ - { - name: "workflow_guide", - description: { zh: "小红书助手工具使用流程指南。工具主要分为几类:\n1. **内容发现**: 使用 `browse_home_feed` 浏览主页,或用 `search_content` 搜索特定内容。\n2. **查看与互动**: 发现帖子后,用 `view_post` 查看详情。在详情页内可执行 `like_post`, `collect_post`, `comment_post`, `follow_user`, `unfollow_user` 等并列互动操作。\n3. **内容发布**: `publish_post` 是一个独立的发布流程。\n4. **导航**: `navigate_to_home`, `back_to_feed` 等工具用于在各页面间跳转。", en: "Workflow guide for the Xiaohongshu assistant:\n1. **Discover**: browse the home feed via `browse_home_feed`, or search via `search_content`.\n2. **View & interact**: after finding a post, open it with `view_post`. On the detail page you can run `like_post`, `collect_post`, `comment_post`, `follow_user`, `unfollow_user`, etc.\n3. **Publish**: `publish_post` is a dedicated publishing flow.\n4. **Navigation**: use tools like `navigate_to_home` and `back_to_feed` to switch between pages." }, - parameters: [] - }, - { - name: "browse_home_feed", - description: { zh: "浏览小红书主页信息流,获取推荐内容", en: "Browse the Xiaohongshu home feed and collect recommended content." }, - parameters: [ - { - name: "scroll_count", - description: { zh: "滚动次数,控制浏览的内容量,默认为3次", en: "Number of scrolls (controls how much content to browse; default: 3)." }, - type: "number", - required: false - }, - { - name: "collect_posts", - description: { zh: "是否收集帖子信息,默认为true", en: "Whether to collect post info (default: true)." }, - type: "boolean", - required: false - } - ] - }, - { - name: "search_content", - description: { zh: "在小红书中搜索内容、用户或话题", en: "Search content, users, or topics in Xiaohongshu." }, - parameters: [ - { - name: "keyword", - description: { zh: "搜索关键词", en: "Search keyword." }, - type: "string", - required: true - }, - { - name: "search_type", - description: { zh: "搜索类型:comprehensive(综合)、note(笔记)、user(用户)、topic(话题)", en: "Search type: comprehensive, note, user, topic." }, - type: "string", - required: false - } - ] - }, - { - name: "view_post", - description: { zh: "查看指定的帖子详情", en: "View details of a specific post." }, - parameters: [ - { - name: "post_title", - description: { zh: "要查看的帖子标题关键词", en: "Keyword to match the post title (optional)." }, - type: "string", - required: false - }, - { - name: "post_index", - description: { zh: "要查看的帖子在列表中的索引位置(从1开始)", en: "Index of the post in the list (1-based, optional)." }, - type: "number", - required: false - } - ] - }, - { - name: "like_post", - description: { zh: "给当前帖子点赞", en: "Like the current post." }, - parameters: [] - }, - { - name: "collect_post", - description: { zh: "收藏当前帖子", en: "Save (collect) the current post." }, - parameters: [ - { - name: "collection_name", - description: { zh: "收藏夹名称,留空则使用默认收藏夹", en: "Collection name (leave empty to use the default collection)." }, - type: "string", - required: false - } - ] - }, - { - name: "follow_user", - description: { zh: "关注当前帖子的作者", en: "Follow the author of the current post." }, - parameters: [] - }, - { - name: "unfollow_user", - description: { zh: "取消关注当前帖子的作者", en: "Unfollow the author of the current post." }, - parameters: [] - }, - { - name: "comment_post", - description: { zh: "在当前帖子下发表评论", en: "Post a comment under the current post." }, - parameters: [ - { - name: "comment_text", - description: { zh: "要发表的评论内容", en: "Comment text to post." }, - type: "string", - required: true - } - ] - }, - { - name: "get_post_info", - description: { zh: "获取当前帖子的详细信息,包括标题、作者、点赞数、评论数、发布日期和热门评论等", en: "Get detailed info of the current post: title, author, likes, comments, publish date, hot comments, etc." }, - parameters: [] - }, - { - name: "publish_post", - description: { zh: "发布新的帖子内容", en: "Publish a new post." }, - parameters: [ - { - name: "content_text", - description: { zh: "帖子文字内容", en: "Post text content." }, - type: "string", - required: true - }, - { - name: "image_paths", - description: { zh: "图片文件路径列表,用逗号分隔", en: "Comma-separated list of image file paths (optional)." }, - type: "string", - required: false - }, - { - name: "tags", - description: { zh: "话题标签,用逗号分隔", en: "Comma-separated topic tags (optional)." }, - type: "string", - required: false - }, - { - name: "location", - description: { zh: "地理位置信息", en: "Location information (optional)." }, - type: "string", - required: false - } - ] - }, - { - name: "navigate_to_home", - description: { zh: "导航到小红书首页", en: "Navigate to the Xiaohongshu home page." }, - parameters: [] - }, - { - name: "navigate_to_profile", - description: { zh: "导航到个人主页", en: "Navigate to the profile page." }, - parameters: [] - }, - { - name: "navigate_to_search", - description: { zh: "导航到搜索页面", en: "Navigate to the search page." }, - parameters: [] - }, - { - name: "navigate_to_publish", - description: { zh: "导航到发布页面", en: "Navigate to the publish page." }, - parameters: [] - }, - { - name: "back_to_feed", - description: { zh: "从帖子详情页返回到信息流列表", en: "Return from the post detail page back to the feed list." }, - parameters: [] - } - ] -}*/ - -interface Array { - at(index: number): T | undefined; -} - -const XiaohongshuAssistant = (function () { - - // 添加 Array.prototype.at 支持 - Array.prototype.at = function (index: number) { - if (index < 0) { - index = this.length + index; - } - return this[index]; - } - - interface Post { - title: string; - author: string; - likes: string; - } - - interface PostInfo { - index: number; - title: string; - author: string; - likes: string; - element: UINode; - } - - interface Comment { - author: string; - content: string; - likes: string; - date: string; - } - - interface ToolResponse { - success: boolean; - message: string; - [key: string]: unknown; - } - - // 小红书应用包名和主要Activity - const XIAOHONGSHU_PACKAGE = "com.xingin.xhs"; - const MAIN_ACTIVITY = "com.xingin.xhs.activity.SplashActivity"; - const HOME_ACTIVITY = "com.xingin.xhs.index.v2.IndexActivityV2"; - - // Helper to create response objects - function createResponse(success: boolean, message: string, data: object | string = {}) { - if (typeof data === 'string') { - return { success: true, message: message, data: data }; - } - return { success, message, ...data }; - } - - // Helper to find a UI element and click it - async function findAndClick(finder: () => Promise): Promise { - const element = await finder(); - if (element) { - await element.click(); - await Tools.System.sleep(1000); - return true; - } - return false; - } - - // Helper to find a UI element by text and click it - async function findAndClickByText(text: string): Promise { - return findAndClick(async () => (await UINode.getCurrentPage()).findByText(text)); - } - - async function ensureMain(packageName: string = XIAOHONGSHU_PACKAGE): Promise { - let pageInfo = await Tools.UI.getPageInfo(); - - // 1. 如果不在小红书app,则启动 - if (pageInfo.packageName !== packageName) { - await Tools.System.startApp(packageName); - await Tools.System.sleep(3000); - pageInfo = await Tools.UI.getPageInfo(); - } - - // 2. 检查是否在首页 - const isAlreadyOnHome = async () => { - const page = await UINode.getCurrentPage(); - const followButton = page.findByContentDesc("关注"); - const discoverButton = page.findByContentDesc("发现"); - const searchButton = page.findByContentDesc("搜索"); - return followButton && discoverButton && searchButton; - }; - - if (await isAlreadyOnHome()) { - console.log("已经在首页。"); - return true; - } - - // 3. 尝试通过返回回到首页 - console.log("尝试返回到首页。"); - for (let i = 0; i < 4; i++) { - await Tools.UI.pressKey("KEYCODE_BACK"); - await Tools.System.sleep(1000); - if (await isAlreadyOnHome()) { - console.log("成功回到首页。"); - return true; - } - pageInfo = await Tools.UI.getPageInfo(); - if (pageInfo.packageName !== packageName) { - console.log("返回时退出了app。"); - break; // Exit loop if we've exited the app - } - } - - // 4. 如果返回失败,重启app - console.log("返回失败,重启app。"); - await Tools.System.stopApp(packageName); - await Tools.System.sleep(1000); - await Tools.System.startApp(packageName); - await Tools.System.sleep(5000); - - pageInfo = await Tools.UI.getPageInfo(); - if (pageInfo.packageName === packageName) { - console.log("App重启成功。"); - return true; - } - - console.error(`无法导航到 ${packageName} 的首页。`); - return false; - } - - // --- 小红书自动化逻辑 --- - let lastBrowseResults: Post[] = []; - - async function browse_home_feed(params: { scroll_count?: number, collect_posts?: boolean }) { - const { scroll_count = 5, collect_posts = false } = params; - const posts: Post[] = []; - console.log(`开始浏览, 滑动次数: ${scroll_count}, 是否收集帖子: ${collect_posts}`); - - for (let i = 0; i < scroll_count; i++) { - console.log(`第 ${i + 1} 次滑动...`); - - if (collect_posts) { - const currentPage = await UINode.getCurrentPage(); - - const searchScopeNode = currentPage; - console.log("将在整个页面范围内查找帖子。"); - - // 为了调试,我们先找到所有 FrameLayout 并打印它们的 contentDesc - const allFrameLayouts = searchScopeNode.findAll(n => n.className === 'FrameLayout'); - console.log(`[调试] 在查找范围内找到了 ${allFrameLayouts.length} 个 FrameLayout 节点。`); - for (const frame of allFrameLayouts) { - if (frame.contentDesc) { - console.log(`[调试] FrameLayout contentDesc: "${frame.contentDesc}"`); - } - } - - // 正式筛选帖子节点 - const postNodes = allFrameLayouts.filter(n => - n.contentDesc && n.contentDesc.includes(' 来自') - ); - - console.log(`本次查找共发现 ${postNodes.length} 个帖子。`); - - for (const postNode of postNodes) { - const desc = postNode.contentDesc; - if (desc) { - try { - const parts = desc.split(' 来自'); - if (parts.length > 1) { - const authorAndLikesStr = parts.pop()!.trim(); - let title = parts.join(' 来自').trim(); - - // 移除 "笔记" or "视频" 前缀, 包括中间有空格或换行的情况 - title = title.replace(/^(?:笔\s*记|视\s*频)\s*/, ''); - - const words = authorAndLikesStr.split(/\s+/); - if (words.length > 0) { - const likesWithSuffix = words.pop(); - const author = words.join(' '); - - if (title && likesWithSuffix && likesWithSuffix.endsWith('赞')) { - const post = { - title, - author: author || "未知作者", - likes: likesWithSuffix.replace('赞', '').trim() - }; - - if (!posts.some(p => p.title === post.title && p.author === post.author)) { - console.log(` -> 收集到新帖子: ${post.title} | 作者: ${post.author} | 赞: ${post.likes}`); - posts.push(post); - } - } - } - } - } catch (e) { - console.log(`解析时出现异常: "${desc}"`, e); - } - } - } - } - - await Tools.UI.swipe(500, 1800, 500, 500); // 从下往上滑动 - await Tools.System.sleep(2500); // 等待新内容加载 - } - - console.log(`浏览结束, 共收集到 ${posts.length} 个独立帖子。`); - return createResponse(true, "成功浏览主页信息流", { posts: posts }); - } - - async function search_content(params: { keyword: string; search_type?: string }): Promise { - const { keyword, search_type = "comprehensive" } = params; - console.log(`搜索内容: ${keyword}, 类型: ${search_type}`); - - // 确保小红书已在前台运行 - if (!await ensureMain()) { - return createResponse(false, "无法启动或切换到小红书"); - } - - // 1. 点击主页右上角的搜索图标 - let page = await UINode.getCurrentPage(); - const searchIcon = page.findByContentDesc("搜索"); - - if (!searchIcon) { - return createResponse(false, "在主页上找不到带 '搜索' contentDesc 的图标"); - } - await searchIcon.click(); - await Tools.System.sleep(2000); // 等待搜索页面加载 - - // 2. 在搜索页面,找到输入框并输入文字 - page = await UINode.getCurrentPage(); - const searchInput = page.findByClass("EditText"); - - if (!searchInput) { - return createResponse(false, "在搜索页面找不到输入框"); - } - - await searchInput.click(); - await Tools.System.sleep(1000); - await Tools.UI.setText(keyword); - await Tools.System.sleep(500); - - // 3. 点击“搜索”按钮 - page = await UINode.getCurrentPage(); - const searchButtons = page.findAllByText("搜索"); - const searchButton = searchButtons.at(-1); // 通常,实际的搜索按钮是最后一个 - - if (!searchButton) { - console.log("找不到文本为'搜索'的按钮,尝试按回车键"); - await Tools.UI.pressKey("KEYCODE_ENTER"); - } else { - await searchButton.click(); - } - await Tools.System.sleep(3000); // 等待搜索结果加载 - - // 应用搜索类型过滤 - if (search_type !== "comprehensive") { - await applySearchFilter(search_type); - } - - // 获取搜索结果 - const results = await getSearchResults(); - - return createResponse(true, `搜索到 ${results.length} 条相关内容`, { - keyword: keyword, - search_type: search_type, - results: results.map(({ element, ...rest }) => rest), // 移除element属性 - result_count: results.length - }); - } - - async function applySearchFilter(filterType: string): Promise { - console.log(`应用搜索过滤器: ${filterType}`); - - const page = await UINode.getCurrentPage(); - let filterButton; - - switch (filterType) { - case "note": - filterButton = page.findByText("笔记"); - break; - case "user": - filterButton = page.findByText("用户"); - break; - case "topic": - filterButton = page.findByText("话题"); - break; - } - - if (filterButton) { - await filterButton.click(); - await Tools.System.sleep(2000); - } - } - - function extractPostInfoFromNode(node: UINode, index: number): PostInfo | null { - // 改进视频帖子过滤逻辑 - const relativeLayout = node.findByClass("RelativeLayout"); - if (relativeLayout) { - const directImageViews = relativeLayout.children.filter(child => child.className === "ImageView"); - if (directImageViews.length > 1) { - // console.log("检测到视频帖子(多个直接ImageView子节点),已跳过。"); - return null; - } - } - - const allTextViews = node.findAllByClass("TextView"); - - // 过滤广告 - if (allTextViews.some(tv => tv.text === "广告")) { - // console.log("检测到广告,已跳过。") - - return null; - } - - if (allTextViews.length < 2) return null; // 需要标题和其他信息 - - // 1. 寻找标题:通常是当中最长的文本 - const sortedByLength = [...allTextViews].sort((a, b) => (b.text?.length || 0) - (a.text?.length || 0)); - const titleNode = sortedByLength[0]; - if (!titleNode || !titleNode.text || titleNode.text.length < 5) return null; - const title = titleNode.text; - - const otherTextViews = allTextViews.filter(tv => tv !== titleNode); - - // 2. 寻找点赞数:包含数字,可能带“万” - const likesRegex = /^\s*\d+(\.\d+)?\s*万?\s*$/; - const likesNode = otherTextViews.find(tv => tv.text && likesRegex.test(tv.text.trim())); - const likes = likesNode?.text?.trim() || "未知"; - - // 3. 寻找作者:不是点赞数,也不是日期 - const dateRegex = /^\d{2,4}-\d{2}(-\d{2})?$/; - const authorNode = otherTextViews.find(tv => - tv.text && - tv !== likesNode && - !dateRegex.test(tv.text.trim()) - ); - const author = authorNode?.text?.trim() || "未知作者"; - - if (title === author) { - return null; - } - - return { - index, - title, - author, - likes, - element: node, - }; - } - - - async function getSearchResults(desiredCount: number = 15): Promise { - const results: PostInfo[] = []; - const seenTitles = new Set(); - - const maxScrolls = 5; - let lastResultCount = -1; - let scrollCount = 0; - - for (let i = 0; i < maxScrolls && results.length < desiredCount; i++) { - const page = await UINode.getCurrentPage(); - const resultsContainer = page.findByClass("RecyclerView"); - - if (!resultsContainer) { - console.log("未找到搜索结果容器 (RecyclerView)"); - if (i === 0) return []; - else break; - } - - for (const item of resultsContainer.children) { - const postInfo = extractPostInfoFromNode(item, results.length + 1); - if (postInfo && !seenTitles.has(postInfo.title)) { - seenTitles.add(postInfo.title); - results.push(postInfo); - if (results.length >= desiredCount) break; - } - } - - if (results.length >= desiredCount) break; - - if (results.length === lastResultCount) { - console.log("滚动未产生新结果,停止滚动"); - break; - } - lastResultCount = results.length; - - await Tools.UI.swipe(540, 1500, 540, 800); - await Tools.System.sleep(2000); - scrollCount++; - } - - console.log(`Scrolling back up ${scrollCount} times.`); - for (let i = 0; i < scrollCount; i++) { - await Tools.UI.swipe(540, 800, 540, 1550); - await Tools.System.sleep(1000); - } - return results; - } - - async function view_post(params: { post_title?: string; post_index?: number }): Promise { - const { post_title, post_index } = params; - - if (!post_title && !post_index) { - return createResponse(false, "请提供帖子标题或索引"); - } - - console.log(`尝试查看帖子 - 标题: "${post_title}", 索引: ${post_index}`); - - let targetPost: { title: string; item: UINode } | null = null; - let currentIndex = 0; - const seenTitles = new Set(); - const maxScrolls = 5; - - for (let i = 0; i < maxScrolls; i++) { - const page = await UINode.getCurrentPage(); - const resultsContainer = page.findByClass("RecyclerView"); - - if (!resultsContainer) { - if (i === 0) return createResponse(false, "未找到帖子列表容器 (RecyclerView)"); - else break; - } - - for (const item of resultsContainer.children) { - const postInfo = extractPostInfoFromNode(item, 0); // index doesn't matter here - if (postInfo && !seenTitles.has(postInfo.title)) { - seenTitles.add(postInfo.title); - currentIndex++; - - let shouldClick = false; - if (post_index && currentIndex === post_index) { - shouldClick = true; - } else if (post_title && postInfo.title.includes(post_title)) { - shouldClick = true; - } - - if (shouldClick) { - targetPost = { title: postInfo.title, item: postInfo.element }; - break; - } - } - } - - if (targetPost) break; - - await Tools.UI.swipe(540, 1500, 540, 800); - await Tools.System.sleep(2000); - } - - - if (targetPost) { - await targetPost.item.click(); - await Tools.System.sleep(3000); // 等待帖子详情页加载 - let res = await get_post_info({}); - - return createResponse(true, `成功打开帖子并完成内容加载: "${targetPost.title}"`, res); - } else { - return createResponse(false, "未找到指定的帖子", { post_title, post_index }); - } - } - - async function isInPostDetail(): Promise { - const page = await UINode.getCurrentPage(); - // 检测是否在帖子详情页的特征元素 - const likeButton = page.findByContentDesc("点赞") || - page.findByText("赞") || - page.find(n => n.contentDesc && n.contentDesc.includes("点赞")); - const commentButton = page.findByContentDesc("评论") || - page.findByText("评论") || - page.find(n => n.contentDesc && n.contentDesc.includes("评论")); - - return !!(likeButton || commentButton); - } - - async function findLikeButton(): Promise { - const page = await UINode.getCurrentPage(); - const button = page.findByContentDesc("点赞") || - page.findByText("赞") || - page.find(n => n.contentDesc && n.contentDesc.includes("点赞")); - return button || null; - } - - async function findCollectButton(): Promise { - const page = await UINode.getCurrentPage(); - const button = page.findByContentDesc("收藏") || - page.findByText("收藏") || - page.find(n => n.contentDesc && n.contentDesc.includes("收藏")); - return button || null; - } - - async function findCommentButton(): Promise { - const page = await UINode.getCurrentPage(); - const button = page.findByContentDesc("评论") || - page.findByText("评论") || - page.find(n => n.contentDesc && n.contentDesc.includes("评论")); - return button || null; - } - - async function like_post(params: {}): Promise { - if (!await isInPostDetail()) { - return createResponse(false, "当前不在帖子详情页"); - } - - console.log("给帖子点赞"); - - const likeButton = await findLikeButton(); - - if (likeButton) { - await likeButton.click(); - await Tools.System.sleep(1000); - return createResponse(true, "点赞成功"); - } else { - return createResponse(false, "未找到点赞按钮"); - } - } - - async function collect_post(params: { collection_name?: string }): Promise { - if (!await isInPostDetail()) { - return createResponse(false, "当前不在帖子详情页"); - } - - const { collection_name } = params; - console.log(`收藏帖子到: ${collection_name || "默认收藏夹"}`); - - const collectButton = await findCollectButton(); - - if (collectButton) { - await collectButton.click(); - await Tools.System.sleep(2000); - - // 如果指定了收藏夹名称,尝试选择 - if (collection_name) { - const collectionPage = await UINode.getCurrentPage(); - const targetCollection = collectionPage.findByText(collection_name); - if (targetCollection) { - await targetCollection.click(); - await Tools.System.sleep(1000); - } - } - - return createResponse(true, "收藏成功", { collection_name }); - } else { - return createResponse(false, "未找到收藏按钮", { collection_name }); - } - } - - async function follow_user(params: {}): Promise { - if (!await isInPostDetail()) { - return createResponse(false, "当前不在帖子详情页"); - } - - console.log("关注用户"); - - const page = await UINode.getCurrentPage(); - const followButton = page.findByText("关注"); - - if (followButton) { - await followButton.click(); - await Tools.System.sleep(1000); - return createResponse(true, "关注成功"); - } else { - return createResponse(false, "未找到关注按钮或已经关注"); - } - } - async function unfollow_user(params: {}): Promise { - if (!await isInPostDetail()) { - return createResponse(false, "当前不在帖子详情页"); - } - - console.log("不关注用户"); - - const page = await UINode.getCurrentPage(); - const followButton = page.findByText("已关注"); - - if (followButton) { - await followButton.click(); - await Tools.System.sleep(1000); - (await UINode.getCurrentPage()).findByText("不再关注")?.click(); - - return createResponse(true, "取消关注成功"); - } else { - return createResponse(false, "未找到不关注按钮或已经不关注"); - } - } - - async function comment_post(params: { comment_text: string }): Promise { - if (!await isInPostDetail()) { - return createResponse(false, "当前不在帖子详情页"); - } - - const { comment_text } = params; - console.log(`发表评论: ${comment_text}`); - - const page = await UINode.getCurrentPage(); - const commentBox = page.findByContentDesc("评论框") - - if (commentBox) { - await commentBox.click(); - await Tools.System.sleep(1000); - - // 输入评论内容 - await Tools.UI.setText(comment_text); - await Tools.System.sleep(500); - - // 查找发送按钮 - const sendButton = (await UINode.getCurrentPage()).findByText("发送") - - if (sendButton) { - await sendButton.click(); - await Tools.System.sleep(2000); - return createResponse(true, "评论发表成功", { comment_text }); - } else { - return createResponse(false, "未找到发送按钮", { comment_text }); - } - } else { - return createResponse(false, "未找到评论输入框", { comment_text }); - } - } - - async function get_post_info(params: GetPostInfoParams): Promise { - if (!await isInPostDetail()) { - return createResponse(false, "当前不在帖子详情页"); - } - console.log("获取帖子信息..."); - - // Initial scroll to load content below the media - await Tools.UI.swipe(540, 1600, 540, 1000); - await Tools.System.sleep(1500); - - const page = await UINode.getCurrentPage(); - - const postInfo: { - title: string; - author: string; - content: string; - like_count: string; - comment_count: string; - collect_count: string; - publish_date: string; - comments: Comment[]; - } = { - title: "未知", - author: "未知", - content: "", - like_count: "未知", - comment_count: "未知", - collect_count: "未知", - publish_date: "未知", - comments: [], - }; - - // 1. Parse static information first - const authorButton = page.find(n => n.contentDesc && n.contentDesc.startsWith("作者,")); - let authorTextView: UINode | undefined; - if (authorButton) { - authorTextView = authorButton.findByClass("TextView") ?? undefined; - if (authorTextView && authorTextView.text) { - postInfo.author = authorTextView.text; - } else if (authorButton.contentDesc) { - const parts = authorButton.contentDesc.split(','); - if (parts.length > 1) { - postInfo.author = parts[1].trim(); - } - } - } - - const allNodesWithDesc = page.findAll(n => !!n.contentDesc); - const dateNode = allNodesWithDesc.find(n => n.contentDesc && /^\d{2}-\d{2}$/.test(n.contentDesc.trim())); - if (dateNode && dateNode.contentDesc) { - postInfo.publish_date = dateNode.contentDesc.trim(); - } - - const findCountFromButton = (button: UINode | null): string => { - if (!button) return "未知"; - if (button.contentDesc) { - const match = button.contentDesc.match(/(\d+(\.\d+)?[万wW]?)/); - if (match && match[1]) return match[1]; - } - const countNode = button.findByClass("TextView"); - if (countNode && countNode.text && /^\d+(\.\d+)?[万wW]?$/.test(countNode.text.trim())) { - return countNode.text.trim(); - } - return "未知"; - }; - - postInfo.like_count = findCountFromButton(await findLikeButton()); - postInfo.comment_count = findCountFromButton(await findCommentButton()); - postInfo.collect_count = findCountFromButton(await findCollectButton()); - - // 2. Identify comment section to isolate title/content - const allRecyclerViews = page.findAllByClass("RecyclerView"); - const commentListInitial = allRecyclerViews.find(rv => { - const parent = rv.parent; - return !(parent && parent.className === 'FrameLayout' && parent.contentDesc && (parent.contentDesc.includes("图片") || parent.contentDesc.includes("视频"))); - }); - - // 3. Extract title and content - const commentTextViews = new Set(commentListInitial ? commentListInitial.findAllByClass("TextView") : []); - const allTextViews = page.findAllByClass("TextView"); - - const mainTextViews = allTextViews.filter(tv => { - return tv && !commentTextViews.has(tv); - }); - - const ignoredTexts = new Set(['关注', '赞', '评论', '收藏', '翻译', '说点什么...', postInfo.author]); - if (postInfo.author === '未知') { - ignoredTexts.delete('未知'); - } - - const contentTextNodes = mainTextViews.filter(tv => { - if (!tv.text) return false; - const text = tv.text.trim(); - return text.length > 0 && !ignoredTexts.has(text) && !/^\d+(\.\d+)?[万wW]?$/.test(text) && tv !== authorTextView; - }); - - if (contentTextNodes.length > 0) { - postInfo.title = contentTextNodes[0].text!.trim(); - if (contentTextNodes.length > 1) { - postInfo.content = contentTextNodes.slice(1).map(tv => tv.text!.trim()).join('\n'); - } - } - - // 4. Scroll and parse comments iteratively - console.log("开始滚动并解析评论..."); - const seenComments = new Set(); - let stableScrolls = 0; - const maxScrolls = 10; - - for (let i = 0; i < maxScrolls; i++) { - const currentCommentCount = postInfo.comments.length; - const currentPage = await UINode.getCurrentPage(); - - const commentList = currentPage.findAllByClass("RecyclerView").find(rv => { - const parent = rv.parent; - return !(parent && parent.className === 'FrameLayout' && parent.contentDesc && (parent.contentDesc.includes("图片") || parent.contentDesc.includes("视频"))); - }); - - if (commentList) { - for (const item of commentList.children) { - if (postInfo.comments.length >= 20) break; - - const textViews = item.findAllByClass("TextView"); - if (textViews.length < 2) continue; - - const comment: Comment = { author: "未知", content: "未知", likes: "0", date: "未知" }; - - const likesRegex = /^\d+(\.\d+)?[万wW]?$/; - const likesNode = item.find(n => n.className === 'TextView' && n.text && likesRegex.test(n.text)); - comment.likes = likesNode?.text ?? "0"; - - const contentCandidates = textViews.filter(tv => tv.text && tv !== likesNode); - if (contentCandidates.length === 0) continue; - - contentCandidates.sort((a, b) => (b.text?.length ?? 0) - (a.text?.length ?? 0)); - - let contentText = contentCandidates[0].text ?? ""; - const authorNode = contentCandidates.length > 1 ? contentCandidates[1] : undefined; - - comment.author = authorNode?.text?.replace("作者", "").trim() || "未知"; - - const dateRegex = /(\d{2}-\d{2}( \d{2}:\d{2})?|\d+天前|昨天 \d{2}:\d{2})/; - let dateMatch = contentText.match(dateRegex); - if (dateMatch) { - comment.date = dateMatch[0]; - comment.content = contentText.replace(dateRegex, ' ').replace(/回复$/, '').trim(); - } else { - comment.content = contentText.replace(/回复$/, '').trim(); - } - - if (comment.content.startsWith(comment.author)) { - comment.content = comment.content.substring(comment.author.length).trim(); - } - - if (comment.content === comment.author || !comment.content) continue; - - const uniqueKey = `${comment.author}:${comment.content}`; - if (comment.content && comment.content !== "未知" && !seenComments.has(uniqueKey)) { - seenComments.add(uniqueKey); - postInfo.comments.push(comment); - } - } - } - - if (postInfo.comments.length >= 20) { - console.log("已收集到20条评论,停止滚动。"); - break; - } - - if (postInfo.comments.length === currentCommentCount) { - stableScrolls++; - if (stableScrolls >= 2) { - console.log("滚动两次未发现新评论,停止。"); - break; - } - } else { - stableScrolls = 0; - } - - console.log(`第 ${i + 1}/${maxScrolls} 次滚动... 当前评论数: ${postInfo.comments.length}`); - await Tools.UI.swipe(540, 1600, 540, 800); - await Tools.System.sleep(2000); - } - - return createResponse(true, "获取帖子信息成功", { post_info: postInfo }); - } - - async function publish_post(params: { content_text: string; image_paths?: string; tags?: string; location?: string }): Promise { - const { content_text, image_paths, tags, location } = params; - - console.log(`发布帖子: ${content_text.substring(0, 20)}...`); - - // 确保小红书已在前台运行 - if (!await ensureMain()) { - return createResponse(false, "无法启动或切换到小红书"); - } - - // 点击发布按钮(通常在底部导航栏中间) - const page = await UINode.getCurrentPage(); - const publishButton = page.findByText("发布") || - page.findByText("+") || - page.findByContentDesc("发布"); - - if (!publishButton) { - return createResponse(false, "未找到发布按钮"); - } - - await publishButton.click(); - await Tools.System.sleep(3000); - - // 选择发布类型(如果需要) - const noteOption = (await UINode.getCurrentPage()).findByText("笔记") || - (await UINode.getCurrentPage()).findByText("图文"); - if (noteOption) { - await noteOption.click(); - await Tools.System.sleep(2000); - } - - // 如果提供了图片路径,添加图片 - if (image_paths) { - const imagePaths = image_paths.split(','); - for (const imagePath of imagePaths) { - // 这里需要根据实际的图片选择界面来实现 - console.log(`添加图片: ${imagePath.trim()}`); - // 实际实现中需要打开相册、选择图片等步骤 - } - } - - // 输入文字内容 - const pageForInput = await UINode.getCurrentPage(); - const textInput = pageForInput.findByClass("EditText") || - pageForInput.findByText("分享你的生活..."); - - if (textInput) { - await textInput.click(); - await Tools.System.sleep(1000); - await Tools.UI.setText(content_text); - await Tools.System.sleep(1000); - } - - // 添加标签 - if (tags) { - const tagList = tags.split(','); - for (const tag of tagList) { - const tagText = tag.trim(); - if (tagText) { - // 在文本末尾添加话题标签 - await Tools.UI.setText(` #${tagText}`); - await Tools.System.sleep(500); - } - } - } - - // 添加位置信息 - if (location) { - const locationButton = (await UINode.getCurrentPage()).findByText("添加地点"); - if (locationButton) { - await locationButton.click(); - await Tools.System.sleep(2000); - // 搜索并选择位置 - await Tools.UI.setText(location); - await Tools.System.sleep(1000); - const firstLocation = (await UINode.getCurrentPage()).findByClass("TextView"); - if (firstLocation && firstLocation.text && firstLocation.text.includes(location)) { - await firstLocation.click(); - await Tools.System.sleep(1000); - } - } - } - - // 发布帖子 - const finalPublishButton = (await UINode.getCurrentPage()).findByText("发布"); - - if (finalPublishButton) { - await finalPublishButton.click(); - await Tools.System.sleep(5000); // 等待发布完成 - - return createResponse(true, "帖子发布成功", { - content_text: content_text, - image_paths: image_paths, - tags: tags, - location: location - }); - } else { - return createResponse(false, "未找到最终发布按钮", { - content_text: content_text - }); - } - } - - async function navigate_to_home(params: {}): Promise { - console.log("导航到首页"); - - if (await ensureMain()) { - const page = await UINode.getCurrentPage(); - const homeTab = page.findByText("首页"); - if (homeTab) { - await homeTab.click(); - await Tools.System.sleep(1000); - } - return createResponse(true, "已导航到首页"); - } else { - return createResponse(false, "无法导航到首页"); - } - } - - async function navigate_to_profile(params: {}): Promise { - console.log("导航到个人主页"); - - if (await ensureMain()) { - const page = await UINode.getCurrentPage(); - const profileTab = page.findByText("我") || - page.findByText("个人主页"); - if (profileTab) { - await profileTab.click(); - await Tools.System.sleep(2000); - return createResponse(true, "已导航到个人主页"); - } else { - return createResponse(false, "未找到个人主页标签"); - } - } else { - return createResponse(false, "无法导航到个人主页"); - } - } - - async function navigate_to_search(params: {}): Promise { - console.log("导航到搜索页面"); - - if (await ensureMain()) { - const page = await UINode.getCurrentPage(); - const searchTab = page.findByText("搜索") || - page.findByContentDesc("搜索"); - - if (searchTab) { - await searchTab.click(); - await Tools.System.sleep(1500); - return createResponse(true, "已导航到搜索页面"); - } else { - // 尝试点击顶部搜索区域 - await Tools.UI.tap(540, 200); - await Tools.System.sleep(1500); - return createResponse(true, "已打开搜索页面"); - } - } else { - return createResponse(false, "无法导航到搜索页面"); - } - } - - async function navigate_to_publish(params: {}): Promise { - console.log("导航到发布页面"); - - if (await ensureMain()) { - const page = await UINode.getCurrentPage(); - const publishTab = page.findByText("发布") || - page.findByText("+"); - - if (publishTab) { - await publishTab.click(); - await Tools.System.sleep(2000); - return createResponse(true, "已导航到发布页面"); - } else { - return createResponse(false, "未找到发布按钮"); - } - } else { - return createResponse(false, "无法导航到发布页面"); - } - } - - async function back_to_feed(params: {}): Promise { - console.log("从帖子详情页返回到信息流"); - - if (await isInPostDetail()) { - await Tools.UI.pressKey("KEYCODE_BACK"); - await Tools.System.sleep(2000); - return createResponse(true, "已返回到信息流列表"); - } else { - return createResponse(true, "当前不在帖子详情页,无需返回"); - } - } - - async function wrapToolExecution

(func: (params: P) => Promise, params: P) { - try { - const result = await func(params); - if (result) { - complete(result); - } else { - complete({ success: true, message: "操作成功完成但无返回数据。" }); - } - } catch (error) { - console.error(`Tool ${func.name} failed unexpectedly`, error); - complete({ - success: false, - message: `工具执行时发生意外错误: ${error instanceof Error ? error.message : String(error)}`, - }); - } - } - - async function main(params: {}): Promise { - console.log("=== 小红书智能助手测试 ==="); - console.log("测试搜索功能..."); - // let res = await search_content({ keyword: "美食" }); - // console.log(JSON.stringify(res, null, 2)); - - // if (res.success && (res.result_count as number) > 0) { - // console.log("测试查看帖子功能..."); - // let view_res = await view_post({ post_index: 10 }); - // console.log(JSON.stringify(view_res, null, 2)); - // } else { - // console.log("搜索无结果,跳过查看帖子测试。"); - // } - console.log(JSON.stringify(await get_post_info({}), null, 2)); - } - - // Tool parameter interfaces - interface BrowseHomeFeedParams { scroll_count?: number; collect_posts?: boolean; } - interface SearchContentParams { keyword: string; search_type?: string; } - interface ViewPostParams { post_title?: string; post_index?: number; } - interface LikePostParams { } - interface CollectPostParams { collection_name?: string; } - interface FollowUserParams { } - interface UnfollowUserParams { } - interface CommentPostParams { comment_text: string; } - interface GetPostInfoParams { } - interface PublishPostParams { content_text: string; image_paths?: string; tags?: string; location?: string; } - interface NavigateToHomeParams { } - interface NavigateToProfileParams { } - interface NavigateToSearchParams { } - interface NavigateToPublishParams { } - interface BackToFeedParams { } - interface MainParams { } - - // 导出所有工具 - return { - browse_home_feed: async (params: BrowseHomeFeedParams) => await wrapToolExecution(browse_home_feed, params), - search_content: async (params: SearchContentParams) => await wrapToolExecution(search_content, params), - view_post: async (params: ViewPostParams) => await wrapToolExecution(view_post, params), - like_post: async (params: LikePostParams) => await wrapToolExecution(like_post, params), - collect_post: async (params: CollectPostParams) => await wrapToolExecution(collect_post, params), - follow_user: async (params: FollowUserParams) => await wrapToolExecution(follow_user, params), - unfollow_user: async (params: UnfollowUserParams) => await wrapToolExecution(unfollow_user, params), - comment_post: async (params: CommentPostParams) => await wrapToolExecution(comment_post, params), - get_post_info: async (params: GetPostInfoParams) => await wrapToolExecution(get_post_info, params), - publish_post: async (params: PublishPostParams) => await wrapToolExecution(publish_post, params), - navigate_to_home: async (params: NavigateToHomeParams) => await wrapToolExecution(navigate_to_home, params), - navigate_to_profile: async (params: NavigateToProfileParams) => await wrapToolExecution(navigate_to_profile, params), - navigate_to_search: async (params: NavigateToSearchParams) => await wrapToolExecution(navigate_to_search, params), - navigate_to_publish: async (params: NavigateToPublishParams) => await wrapToolExecution(navigate_to_publish, params), - back_to_feed: async (params: BackToFeedParams) => await wrapToolExecution(back_to_feed, params), - main: async (params: MainParams) => await wrapToolExecution(main, params), - }; -})(); - -// 逐个导出,以便在外部调用 -exports.browse_home_feed = XiaohongshuAssistant.browse_home_feed; -exports.search_content = XiaohongshuAssistant.search_content; -exports.view_post = XiaohongshuAssistant.view_post; -exports.like_post = XiaohongshuAssistant.like_post; -exports.collect_post = XiaohongshuAssistant.collect_post; -exports.follow_user = XiaohongshuAssistant.follow_user; -exports.unfollow_user = XiaohongshuAssistant.unfollow_user; -exports.comment_post = XiaohongshuAssistant.comment_post; -exports.get_post_info = XiaohongshuAssistant.get_post_info; -exports.publish_post = XiaohongshuAssistant.publish_post; -exports.navigate_to_home = XiaohongshuAssistant.navigate_to_home; -exports.navigate_to_profile = XiaohongshuAssistant.navigate_to_profile; -exports.navigate_to_search = XiaohongshuAssistant.navigate_to_search; -exports.navigate_to_publish = XiaohongshuAssistant.navigate_to_publish; -exports.back_to_feed = XiaohongshuAssistant.back_to_feed; -exports.main = XiaohongshuAssistant.main; \ No newline at end of file diff --git a/scripts/baidu_map.ts b/scripts/baidu_map.ts deleted file mode 100644 index 375e315..0000000 --- a/scripts/baidu_map.ts +++ /dev/null @@ -1,1443 +0,0 @@ -/* -METADATA -{ - "name": "baidu_map", - "display_name": { - "zh": "百度地图工具", - "en": "Baidu Map Tools" - }, - "description": { "zh": "百度地图工具集合,提供AOI(兴趣区域)数据获取接口。通过调用百度地图API,支持按地理范围查询AOI边界坐标,基于位置的路线规划,助力地理信息系统应用开发和空间数据分析。", "en": "A Baidu Maps toolkit that provides AOI (Area of Interest) data access. It supports querying AOI boundary coordinates by geographic range and location-based route planning, useful for GIS development and spatial data analysis." }, - "enabledByDefault": true, - "category": "Map", - "tools": [ - { - "name": "search_aoi", - "description": { "zh": "搜索百度地图兴趣区域(AOI)信息", "en": "Search AOI (Area of Interest) information from Baidu Maps." }, - "parameters": [ - { - "name": "keyword", - "description": { "zh": "搜索关键词,如商场、小区名称等", "en": "Search keyword, e.g. mall name, residential community name, etc." }, - "type": "string", - "required": true - }, - { - "name": "city_name", - "description": { "zh": "城市名称,如'北京',默认全国范围", "en": "City name, e.g. '北京'. Defaults to nationwide." }, - "type": "string", - "required": false - } - ] - }, - { - "name": "planRoute", - "description": { "zh": "智能路线规划,从当前位置到指定目的地,并发返回驾车、步行、公交三种方式的路线规划。", "en": "Smart route planning from current location to a destination, returning driving/walking/transit plans in parallel." }, - "parameters": [ - { - "name": "destination", - "description": { "zh": "目的地名称", "en": "Destination name." }, - "type": "string", - "required": true - }, - { - "name": "city_name", - "description": { "zh": "城市名称,辅助目的地查找", "en": "City name to help resolve the destination (optional)." }, - "type": "string", - "required": false - } - ] - }, - ] -}*/ - -// Baidu Map API Type Definitions -namespace BaiduMap { - export interface Point { - x: string; - y: string; - } - - export interface TicketInfo { - title?: string; - price?: string; - marketprice?: string; - sold_info?: string; - bookingTimeTag?: string[]; - ticket_type_name?: string; - } - - export interface RankingInfo { - ranking?: string; - rank_s?: string; - score?: number; - short_name?: string; - ranking_type?: string; - } - - export interface EventNotice { - title?: string; - content?: string; - img_url?: string; - start_end?: string; - } - - export interface BangdanHead { - ranking_show?: RankingInfo[]; - } - - export interface DetailInfo { - point?: Point; - guoke_geo?: { - geo: string; - }; - tag?: string; - overall_rating?: string; - comment_num?: string; - price?: string; - shop_hours_simple?: string; - photo_num?: string; - indoor_map?: string; - aoi_src_id?: string; - navi_update_time?: string; - official_url?: string; - is_reservable?: string; - dk_ticket?: TicketInfo; - bangdan_head?: BangdanHead; - event_notice?: EventNotice; - areaid?: string; - entrance_price?: string; - free?: string; - } - - export interface Ext { - detail_info?: DetailInfo; - geo?: string; - } - - export interface BusinessTimeInfo { - common?: string; - festival?: string; - } - - export interface BusinessTimeData { - business_time_text?: BusinessTimeInfo; - common?: BusinessTimeSchedule[]; - festival?: Record; - } - - export interface BusinessTimeSchedule { - date?: string; - season?: string; - special?: string; - weeks?: Record; - } - - export interface TimeSlot { - time?: string; - type?: string; - } - - export interface BusinessTime { - data?: BusinessTimeData[]; - } - - export interface ContentItem { - uid?: string; - name?: string; - alias?: string; - addr?: string; - area_name?: string; - tel?: string; - cla?: [number, string][]; - ext?: Ext; - geo?: string; - geodata?: unknown; - guoke_geo?: { geo: string }; - geo_type?: number; - x?: string; - y?: string; - point?: Point; - di_tag?: string; - std_tag?: string; - overall_rating?: string; - business_time?: BusinessTime; - shop_hours_simple?: string; - pano?: string | number; - area_code?: string | number; - city_id?: string | number; - [key: string]: unknown; - } - - export interface ApiResponse { - content?: ContentItem[]; - current_city?: { - code?: string | number; - }; - result?: { - code?: string | number; - city_id?: string | number; - }; - [key: string]: unknown; - } -} - -/** - * 门票信息接口 - */ -interface TicketInfoResult { - title?: string; - price?: string; - market_price?: string; - sold_info?: string; - booking_tags: string[]; - ticket_type?: string; -} - -/** - * 排行榜信息接口 - */ -interface RankingResult { - name?: string; - rank?: string; - score?: number; - short_name?: string; - type?: string; -} - -/** - * 活动事件接口 - */ -interface EventResult { - title?: string; - content?: string; - img_url?: string; - start_end?: string; -} - -/** - * 详细开放时间接口 - */ -interface OpeningHoursDetail { - common_hours?: string; - festival_hours?: string; - detailed_schedule: BaiduMap.BusinessTimeSchedule[]; - festival_schedule: Record; -} - -/** - * AOI (Area of Interest) item structure returned from search. - */ -interface AoiDataItem { - uid: string; - name: string; - address: string; - area_name?: string; - phone?: string; - tags?: string; - detail_type?: string; - rating?: number; - comment_count?: number; - price?: string; - ticket_info?: TicketInfoResult; - opening_hours?: string; - opening_hours_detail?: OpeningHoursDetail; - shop_hours_simple?: string; - photo_count?: number; - has_street_view?: boolean; - street_view_info?: { pid: string; heading: number }; - has_indoor_map?: boolean; - rankings: RankingResult[]; - events: EventResult[]; - type: string; - has_geo_data: boolean; - center: { - lng: number; - lat: number; - }; - detail_url: string; - additional_info: Record; - raw_data?: Partial; -} - -/** - * Result structure for the `search_aoi` function. - */ -interface SearchAoiResult { - success: boolean; - message?: string; - keyword: string; - city_name?: string; - total: number; - aois: AoiDataItem[]; - boundary?: AoiBoundaryResult; -} - -/** - * Result structure for the `get_aoi_boundary` function. - */ -interface AoiBoundaryResult { - success: boolean; - message?: string; - uid: string; - name?: string; - address?: string; - center?: { - lng: number; - lat: number; - }; - boundary?: { lng: number, lat: number }[]; - point_count?: number; - raw_data?: Partial; -} - -/** - * Location information structure. - */ -interface LocationInfo { - lat: number; - lng: number; - address?: string; -} - -/** - * Route details for a specific transport mode. - */ -interface RouteDetails { - estimated_distance: string; - estimated_duration: string; - transport_mode: "driving" | "walking" | "transit"; - navigation_url: string; - suggestion: string; -} - -/** - * Destination information structure. - */ -interface DestinationInfo { - name: string; - address: string; - location: LocationInfo; -} - -/** - * Route planning result structure. - */ -interface RouteResult { - success: boolean; - message?: string; - current_location?: LocationInfo; - destination?: DestinationInfo; - all_routes?: { - driving: RouteDetails; - walking: RouteDetails; - transit: RouteDetails; - }; -} - -/** - * Test results structure. - */ -interface BaiduMapTestResults { - aoi_search?: { - success: boolean; - total?: number; - error?: string; - }; - advanced_feature?: { - success: boolean; - uid?: string; - point_count?: number; - error?: string; - }; - route_planning?: { - success: boolean; - destination?: string; - distance?: string; - duration?: string; - error?: string; - }; - route_planning_all_modes?: { - success: boolean; - destination?: string; - routes?: { - driving: RouteDetails; - walking: RouteDetails; - transit: RouteDetails; - }; - error?: string; - }; -} - - -const baiduMap = (function () { - // 常用城市编码 - const CITY_CODES = { - "北京": "131", - "上海": "289", - "广州": "257", - "深圳": "340", - "杭州": "179", - "南京": "315", - "武汉": "218", - "成都": "75", - "重庆": "132", - "西安": "233", - "全国": "1" // 默认值 - }; - - // 请求头配置 - const HEADERS = { - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', - 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', - 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - 'Host': 'map.baidu.com', - 'Pragma': 'no-cache', - 'Sec-Fetch-Dest': 'document', - 'Sec-Fetch-Mode': 'navigate', - 'Sec-Fetch-Site': 'none', - 'Sec-Fetch-User': '?1', - 'Upgrade-Insecure-Requests': '1', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0', - 'sec-ch-ua': '"Chromium";v="136", "Microsoft Edge";v="136", "Not.A/Brand";v="99"', - 'sec-ch-ua-mobile': '?0', - 'sec-ch-ua-platform': '"Windows"', - 'Referer': 'https://map.baidu.com/' - }; - - // 日志级别配置 - const LOG_LEVELS = { - NONE: 0, // 不输出任何日志 - ERROR: 1, // 只输出错误信息 - WARN: 2, // 输出警告和错误 - INFO: 3, // 输出普通信息、警告和错误 - DEBUG: 4, // 输出调试信息、普通信息、警告和错误 - TRACE: 5 // 输出所有日志,包括跟踪信息 - }; - - // 默认日志级别 - let currentLogLevel = LOG_LEVELS.INFO; - - /** - * 设置日志级别 - */ - function setLogLevel(level: number): void { - if (level >= LOG_LEVELS.NONE && level <= LOG_LEVELS.TRACE) { - currentLogLevel = level; - } - } - - /** - * 统一的日志输出函数 - */ - function logger(level: number, message: string, data?: unknown): void { - if (level > currentLogLevel) return; - - const levelNames = { - [LOG_LEVELS.ERROR]: '[ERROR]', - [LOG_LEVELS.WARN]: '[WARN]', - [LOG_LEVELS.INFO]: '[INFO]', - [LOG_LEVELS.DEBUG]: '[DEBUG]', - [LOG_LEVELS.TRACE]: '[TRACE]' - }; - - const levelName = levelNames[level] || '[UNKNOWN]'; - const timestamp = new Date().toISOString(); - const logMessage = `${timestamp} ${levelName} ${message}`; - - console.log(logMessage); - } - - /** - * 对中文进行URL编码 - */ - function encodeURIComponentSafe(str: string): string { - try { - return encodeURIComponent(str); - } catch (e) { - logger(LOG_LEVELS.ERROR, `编码失败:`, e); - return str; - } - } - - /** - * 使用OkHttp发起GET请求 - */ - async function httpGet(url: string): Promise { - try { - const client = createHttpClient(); - const response = await client.get(url, HEADERS); - - if (!response.isSuccessful()) { - throw new Error(`请求失败: ${response.statusCode} ${response.statusMessage}`); - } - - try { - const jsonResponse = response.json(); - return jsonResponse; - } catch (e) { - logger(LOG_LEVELS.ERROR, `解析JSON失败:`, e); - return response.content; - } - } catch (e) { - logger(LOG_LEVELS.ERROR, `网络请求错误:`, e); - throw e; - } - } - - /** - * 搜索百度地图AOI信息 - * 参考 https://blog.csdn.net/Jacey_cai/article/details/131524758 - */ - async function search_aoi(params: { - keyword: string; - city_name?: string; // 城市名,如"北京" - }): Promise { - try { - // 参数处理 - const keyword = params.keyword; - if (!keyword) { - throw new Error("关键词不能为空"); - } - - // 处理城市编码 - 改进版 - let cityCode = "1"; // 默认全国 - - if (params.city_name) { - // 使用动态获取城市编码功能 - cityCode = await getCityCode(params.city_name); - logger(LOG_LEVELS.DEBUG, `城市 "${params.city_name}" 对应的编码:`, cityCode); - } - - // 构建URL - 使用搜索API - const encodedKeyword = encodeURIComponentSafe(keyword); - const url = `https://map.baidu.com/?newmap=1&qt=s&da_src=searchBox.button&wd=${encodedKeyword}&c=${cityCode}`; - - logger(LOG_LEVELS.INFO, `搜索AOI: ${keyword}, 城市名称: ${params.city_name || '全国'}`); - - // 发起请求 - const result = await httpGet(url) as BaiduMap.ApiResponse; - - // 根据实际结构,直接使用content数组 - const dataContent = result?.content || []; - - // 如果没有找到任何内容数据结构,返回空结果 - if (!dataContent || dataContent.length === 0) { - logger(LOG_LEVELS.INFO, `搜索结果为空或格式不符合预期`); - return { - success: true, - keyword: keyword, - city_name: params.city_name, - total: 0, - aois: [] as AoiDataItem[] - }; - } - - // 解析content数组中的每个元素 - const potentialAois: AoiDataItem[] = dataContent.map((item: BaiduMap.ContentItem) => { - // ==========基本信息========== - const uid = item.uid || ""; - const name = item.name || (item.alias) || ""; - const address = item.addr || ""; - const area_name = item.area_name; - const phone = item.tel; - - // ==========分类信息========== - let type = ""; - if (item.cla && Array.isArray(item.cla) && item.cla.length > 0) { - type = item.cla.map(cla => cla[1]).join(", "); - } - const detail_type = item.std_tag; - const tags = item.di_tag; - - // ==========评分和评论========== - let rating: number | undefined = undefined; - let comment_count: number | undefined = undefined; - - // 从多个位置提取评分 - const overallRating = item.overall_rating || item.ext?.detail_info?.overall_rating; - if (overallRating) { - rating = parseFloat(overallRating); - } - - // 从多个位置提取评论数 - const commentNum = item.ext?.detail_info?.comment_num; - if (commentNum) { - comment_count = parseInt(commentNum, 10); - } - - // ==========价格信息========== - let price: string | undefined = undefined; - let ticket_info: TicketInfoResult | undefined = undefined; - - // 从detail_info提取价格 - if (item.ext?.detail_info?.price) { - price = item.ext.detail_info.price; - } - - // 提取门票信息 - if (item.ext?.detail_info?.dk_ticket) { - const ticketData = item.ext.detail_info.dk_ticket; - ticket_info = { - title: ticketData.title, - price: ticketData.price, - market_price: ticketData.marketprice, - sold_info: ticketData.sold_info, - booking_tags: ticketData.bookingTimeTag || [], - ticket_type: ticketData.ticket_type_name - }; - } - - // ==========开放时间========== - let opening_hours: string | undefined = undefined; - let opening_hours_detail: OpeningHoursDetail | undefined = undefined; - - if (item.business_time?.data?.[0]) { - const timeData = item.business_time.data[0]; - opening_hours = timeData.business_time_text?.common; - - // 详细开放时间信息 - opening_hours_detail = { - common_hours: timeData.business_time_text?.common, - festival_hours: timeData.business_time_text?.festival, - detailed_schedule: timeData.common || [], - festival_schedule: timeData.festival || {} - }; - } - - // ==========排行榜信息========== - let rankings: RankingResult[] = []; - if (item.ext?.detail_info?.bangdan_head?.ranking_show) { - rankings = item.ext.detail_info.bangdan_head.ranking_show.map((rank: BaiduMap.RankingInfo) => ({ - name: rank.ranking, - rank: rank.rank_s, - score: rank.score, - short_name: rank.short_name, - type: rank.ranking_type - })); - } - - // ==========活动事件========== - let events: EventResult[] = []; - if (item.ext?.detail_info?.event_notice) { - events.push({ - title: item.ext.detail_info.event_notice.title, - content: item.ext.detail_info.event_notice.content, - img_url: item.ext.detail_info.event_notice.img_url, - start_end: item.ext.detail_info.event_notice.start_end - }); - } - - // ==========照片和街景========== - const shop_hours_simple = item.shop_hours_simple; - const photo_count = item.ext?.detail_info?.photo_num ? parseInt(item.ext.detail_info.photo_num, 10) : undefined; - const has_indoor_map = item.ext?.detail_info?.indoor_map === '1'; - - let has_street_view = false; - let street_view_info: { pid: string; heading: number } | undefined = undefined; - if (typeof item.pano === 'string' && item.pano) { - const firstPano = item.pano.split(';')[0]; - const panoParts = firstPano.split(','); - if (panoParts.length >= 2 && panoParts[0]) { - has_street_view = true; - street_view_info = { - pid: panoParts[0], - heading: parseInt(panoParts[1] || '0', 10) - }; - } - } - - // ==========坐标信息========== - let lng = 0, lat = 0; - if (item.ext && item.ext.detail_info && item.ext.detail_info.point) { - lng = parseFloat(item.ext.detail_info.point.x); - lat = parseFloat(item.ext.detail_info.point.y); - } else if (item.x && item.y) { - lng = parseFloat(item.x); - lat = parseFloat(item.y); - } else if (item.point && item.point.x && item.point.y) { - lng = parseFloat(item.point.x); - lat = parseFloat(item.point.y); - } - - // ==========地理数据========== - const hasGeoData = !!( - (item.geo && item.geo.length > 0) || - (item.ext && item.ext.detail_info && item.ext.detail_info.guoke_geo && item.ext.detail_info.guoke_geo.geo) || - item.geo_type == 2 - ); - - // ==========其他附加信息========== - const additional_info: Record = {}; - if (item.ext?.detail_info?.aoi_src_id) additional_info['aoi_src_id'] = item.ext.detail_info.aoi_src_id; - if (item.ext?.detail_info?.navi_update_time) additional_info['navi_update_time'] = item.ext.detail_info.navi_update_time; - if (item.ext?.detail_info?.official_url) additional_info['official_url'] = item.ext.detail_info.official_url; - if (item.ext?.detail_info?.is_reservable) additional_info['is_reservable'] = item.ext.detail_info.is_reservable === '1'; - if (item.ext?.detail_info?.areaid) additional_info['area_id'] = item.ext.detail_info.areaid; - if (item.ext?.detail_info?.entrance_price) additional_info['entrance_price'] = item.ext.detail_info.entrance_price; - if (item.ext?.detail_info?.free) additional_info['is_free'] = item.ext.detail_info.free; - - // 构建详情URL - const detailUrl = uid ? `https://map.baidu.com/?qt=ext&uid=${uid}` : ""; - - return { - uid: uid, - name: name, - address: address, - area_name: area_name, - phone: phone, - tags: tags, - detail_type: detail_type, - rating: rating, - comment_count: comment_count, - price: price, - ticket_info: ticket_info, - opening_hours: opening_hours, - opening_hours_detail: opening_hours_detail, - shop_hours_simple: shop_hours_simple, - photo_count: photo_count, - has_street_view: has_street_view, - street_view_info: street_view_info, - has_indoor_map: has_indoor_map, - rankings: rankings, - events: events, - type: type, - has_geo_data: hasGeoData, - center: { - lng: lng, - lat: lat - }, - detail_url: detailUrl, - additional_info: additional_info, - // 只在DEBUG模式下包含简化的原始数据 - ...(currentLogLevel >= LOG_LEVELS.DEBUG ? { - raw_data: { - uid: item.uid, - name: item.name, - addr: item.addr, - x: item.x, - y: item.y, - geo_type: item.geo_type - } - } : {}) - } as AoiDataItem; - }); - - logger(LOG_LEVELS.DEBUG, `找到${potentialAois.length}个AOI结果`); - - return { - success: true, - keyword: keyword, - city_name: params.city_name, - total: potentialAois.length, - aois: potentialAois - }; - } catch (error) { - logger(LOG_LEVELS.ERROR, `[search_aoi] 错误:`, error); - logger(LOG_LEVELS.ERROR, `错误堆栈:`, error.stack); - - return { - success: false, - message: `搜索AOI失败: ${error.message}`, - keyword: params.keyword, - city_name: params.city_name, - total: 0, - aois: [] as AoiDataItem[] - }; - } - } - - /** - * 获取AOI边界坐标 - */ - async function get_aoi_boundary(params: { uid: string }): Promise { - try { - const uid = params.uid; - if (!uid) { - throw new Error("AOI的UID不能为空"); - } - - // 构建URL - 使用百度地图地点详情接口 - const url = `https://map.baidu.com/?qt=ext&uid=${uid}`; - - logger(LOG_LEVELS.INFO, `获取AOI边界: ${uid}`); - - // 发起请求 - const result = await httpGet(url) as BaiduMap.ApiResponse; - - // 记录完整响应以便调试 - logger(LOG_LEVELS.DEBUG, `AOI边界结果结构:`, Object.keys(result || {})); - - // 检查是否有内容数据 - if (!result || !result.content || (Array.isArray(result.content) && result.content.length === 0)) { - return { - success: false, - message: "未找到AOI边界数据", - uid: uid - }; - } - - // content在新的API中可能是数组,也可能是对象 - const content = (Array.isArray(result.content) ? result.content[0] : result.content) as BaiduMap.ContentItem; - - if (!content) { - return { - success: false, - message: "AOI内容数据为空", - uid: uid - }; - } - - // 尝试多种可能的地理数据结构 - let geoData: string | ({ x?: string | number; y?: string | number; } | (string | number)[])[] | undefined = content.geo; - if (!geoData && content.ext && content.ext.geo) { - geoData = content.ext.geo; - } else if (!geoData && content.geodata) { - geoData = content.geodata as string | ({ x?: string | number; y?: string | number; } | (string | number)[])[]; - } else if (!geoData && content.guoke_geo && content.guoke_geo.geo) { - geoData = content.guoke_geo.geo; - } - - let boundary: Array<{ lng: number, lat: number }> = []; - - if (typeof geoData === 'string') { - // 解析百度地图的geo字符串格式 - // 格式: "4|12957496.191084,4826145.210198;12958713.912140,4828039.128871|1-12957512.1751599,4827980.8872594,..." - try { - const parts = geoData.split('|'); - if (parts.length >= 3) { - // 第三部分包含详细的边界坐标 - const boundaryPart = parts[2]; - const coordinatePairs = boundaryPart.split(','); - - // 每两个数字组成一个坐标点 - for (let i = 0; i < coordinatePairs.length - 1; i += 2) { - const lng = parseFloat(coordinatePairs[i]); - const lat = parseFloat(coordinatePairs[i + 1]); - - if (!isNaN(lng) && !isNaN(lat)) { - boundary.push({ lng, lat }); - } - } - - logger(LOG_LEVELS.DEBUG, `从geo字符串解析到${boundary.length}个边界点`); - } - } catch (e) { - logger(LOG_LEVELS.ERROR, `解析geo字符串失败:`, e); - } - } else if (Array.isArray(geoData)) { - // 尝试解析数组形式的边界 - boundary = geoData.map(point => ({ - lng: parseFloat(String((Array.isArray(point) ? point[0] : point.x) || 0)), - lat: parseFloat(String((Array.isArray(point) ? point[1] : point.y) || 0)) - })); - logger(LOG_LEVELS.DEBUG, `解析到${boundary.length}个边界点`); - } - - return { - success: true, - uid: uid, - name: content.name || "", - address: content.addr || "", - center: { - lng: parseFloat(content.x || (content.point && content.point.x) || '0'), - lat: parseFloat(content.y || (content.point && content.point.y) || '0') - }, - boundary: boundary, - point_count: boundary.length, - // 只在DEBUG模式下包含简化的原始数据 - ...(currentLogLevel >= LOG_LEVELS.DEBUG ? { - raw_data: { - uid: content.uid, - name: content.name, - addr: content.addr, - x: content.x, - y: content.y, - geo_type: content.geo_type - } - } : {}) - }; - } catch (error) { - logger(LOG_LEVELS.ERROR, `[get_aoi_boundary] 错误:`, error); - logger(LOG_LEVELS.ERROR, `错误堆栈:`, error.stack); - - return { - success: false, - message: `获取AOI边界失败: ${error.message}`, - uid: params.uid - }; - } - } - - function wrap(coreFunction: (params: any) => Promise) { - return async (params: any) => { - const result = await coreFunction(params); - complete(result); - return result; - }; - } - /** - * 格式化AOI搜索结果为结构化文本 - */ - function formatAoiResultAsText(aoiResult: SearchAoiResult): string { - if (!aoiResult.success) { - return `AOI搜索失败: ${aoiResult.message || '未知错误'}`; - } - - let output = `=== AOI搜索结果 ===\n`; - output += `搜索关键词: ${aoiResult.keyword}\n`; - output += `搜索城市: ${aoiResult.city_name || '全国'}\n`; - output += `找到结果: ${aoiResult.total} 个\n\n`; - - if (aoiResult.aois && aoiResult.aois.length > 0) { - aoiResult.aois.forEach((aoi, index) => { - output += `--- 结果 ${index + 1} ---\n`; - output += `名称: ${aoi.name}\n`; - output += `类型: ${aoi.type || '未知'}\n`; - output += `地址: ${aoi.address}\n`; - - if (aoi.area_name) { - output += `所属区域: ${aoi.area_name}\n`; - } - - if (aoi.phone) { - output += `联系电话: ${aoi.phone}\n`; - } - - if (aoi.tags) { - output += `标签: ${aoi.tags}\n`; - } - - if (aoi.rating !== undefined) { - output += `评分: ${aoi.rating}/5.0`; - if (aoi.comment_count !== undefined) { - output += ` (${aoi.comment_count}条评论)`; - } - output += `\n`; - } - - if (aoi.price) { - output += `价格信息: ${aoi.price}\n`; - } - - if (aoi.ticket_info) { - output += `门票信息:\n`; - if (aoi.ticket_info.title) output += ` - 门票名称: ${aoi.ticket_info.title}\n`; - if (aoi.ticket_info.price) output += ` - 门票价格: ${aoi.ticket_info.price}\n`; - if (aoi.ticket_info.market_price) output += ` - 市场价: ${aoi.ticket_info.market_price}\n`; - if (aoi.ticket_info.sold_info) output += ` - 销售信息: ${aoi.ticket_info.sold_info}\n`; - } - - if (aoi.opening_hours) { - output += `开放时间: ${aoi.opening_hours}\n`; - } else if (aoi.shop_hours_simple) { - output += `当前状态: ${aoi.shop_hours_simple}\n`; - } - - if (aoi.opening_hours_detail) { - const detail = aoi.opening_hours_detail; - if (detail.common_hours) { - output += `常规时间: ${detail.common_hours}\n`; - } - if (detail.festival_hours) { - output += `节假日时间: ${detail.festival_hours}\n`; - } - } - - if (aoi.photo_count) { - output += `照片数量: ${aoi.photo_count} 张\n`; - } - - const features: string[] = []; - if (aoi.has_street_view) features.push('街景'); - if (aoi.has_indoor_map) features.push('室内地图'); - if (aoi.has_geo_data) features.push('边界数据'); - if (features.length > 0) { - output += `可用功能: ${features.join(', ')}\n`; - } - - if (aoi.rankings && aoi.rankings.length > 0) { - output += `排行榜信息:\n`; - aoi.rankings.slice(0, 3).forEach(rank => { - if (rank.name && rank.rank) { - output += ` - ${rank.name}: ${rank.rank}`; - if (rank.score) output += ` (评分: ${rank.score})`; - output += `\n`; - } - }); - } - - if (aoi.events && aoi.events.length > 0 && aoi.events[0].title) { - output += `当前活动:\n`; - aoi.events.forEach(event => { - if (event.title) { - output += ` - ${event.title}`; - if (event.start_end) output += ` (${event.start_end})`; - output += `\n`; - if (event.content) output += ` ${event.content}\n`; - } - }); - } - - output += `坐标: 经度 ${aoi.center.lng}, 纬度 ${aoi.center.lat}\n`; - output += `详情链接: ${aoi.detail_url}\n`; - - if (Object.keys(aoi.additional_info).length > 0) { - output += `其他信息: `; - const info: string[] = []; - for (const [key, value] of Object.entries(aoi.additional_info)) { - if (key === 'is_free' && value === 2) { - info.push('收费景点'); - } else if (key === 'is_free' && value === 1) { - info.push('免费景点'); - } else if (key === 'area_id') { - info.push(`区域ID: ${value}`); - } else { - info.push(`${key}: ${value}`); - } - } - output += info.join(', ') + `\n`; - } - - output += `\n`; - }); - - if (aoiResult.boundary) { - output += `=== 边界信息 ===\n`; - output += `边界点数: ${aoiResult.boundary.point_count || 0}\n`; - if (aoiResult.boundary.center) { - output += `中心坐标: 经度 ${aoiResult.boundary.center.lng}, 纬度 ${aoiResult.boundary.center.lat}\n`; - } - output += `\n`; - } - } - - return output; - } - - /** - * 格式化路线规划结果为结构化文本 - */ - function formatRouteResultAsText(routeResult: RouteResult): string { - if (!routeResult.success) { - return `路线规划失败: ${routeResult.message || '未知错误'}`; - } - - let output = `=== 路线规划结果 ===\n`; - - if (routeResult.current_location) { - output += `当前位置: 经度 ${routeResult.current_location.lng}, 纬度 ${routeResult.current_location.lat}\n`; - if (routeResult.current_location.address) { - output += `当前地址: ${routeResult.current_location.address}\n`; - } - } - - if (routeResult.destination) { - output += `目的地: ${routeResult.destination.name}\n`; - output += `目的地址: ${routeResult.destination.address}\n`; - output += `目的坐标: 经度 ${routeResult.destination.location.lng}, 纬度 ${routeResult.destination.location.lat}\n`; - } - - output += `\n`; - - if (routeResult.all_routes) { - output += `=== 所有交通方式 ===\n`; - - // 驾车路线 - if (routeResult.all_routes.driving) { - const driving = routeResult.all_routes.driving; - output += `🚗 驾车路线:\n`; - output += ` 距离: ${driving.estimated_distance}\n`; - output += ` 时间: ${driving.estimated_duration}\n`; - output += ` 建议: ${driving.suggestion}\n\n`; - } - - // 步行路线 - if (routeResult.all_routes.walking) { - const walking = routeResult.all_routes.walking; - output += `🚶 步行路线:\n`; - output += ` 距离: ${walking.estimated_distance}\n`; - output += ` 时间: ${walking.estimated_duration}\n`; - output += ` 建议: ${walking.suggestion}\n\n`; - } - - // 公交路线 - if (routeResult.all_routes.transit) { - const transit = routeResult.all_routes.transit; - output += `🚌 公共交通:\n`; - output += ` 距离: ${transit.estimated_distance}\n`; - output += ` 时间: ${transit.estimated_duration}\n`; - output += ` 建议: ${transit.suggestion}\n\n`; - } - } - - return output; - } - - - /** - * 测试格式化函数 - */ - async function main(): Promise { - let output = ""; - output += "========== 百度地图格式化函数测试 ==========\n\n"; - - try { - // 测试AOI搜索格式化 - output += "[1] 测试AOI搜索格式化...\n"; - const aoiResult = await search_aoi({ - keyword: "长安大学", - city_name: "西安" - }); - output += formatAoiResultAsText(aoiResult) + "\n"; - - await Tools.System.sleep(1000); - - // 测试路径规划格式化 - output += "[3] 测试路径规划格式化...\n"; - const routeResult = await planRoute({ - destination: "长安大学", - city_name: "西安" - }); - output += formatRouteResultAsText(routeResult) + "\n"; - - output += "========== 格式化测试完成 ==========\n"; - - logger(LOG_LEVELS.INFO, output); - return output; - } catch (error) { - return `测试过程中发生错误: ${error.message}`; - } - } - - /** - * 获取用户当前位置 - * 使用系统提供的位置API获取当前位置坐标 - */ - async function getCurrentLocation(): Promise<{ lng: number, lat: number } | undefined> { - try { - logger(LOG_LEVELS.INFO, "正在获取用户当前位置..."); - const locationResult = await Tools.System.getLocation(); - - if (!locationResult) { - logger(LOG_LEVELS.ERROR, "获取位置失败:", "未知错误"); - return undefined; - } - - // 获取成功,返回经纬度 - return { - lng: locationResult.longitude, - lat: locationResult.latitude - }; - } catch (error) { - logger(LOG_LEVELS.ERROR, "获取位置出错:", error.message); - return undefined; - } - } - - /** - * 将百度墨卡托坐标转换为WGS-84坐标系下的经纬度(近似) - * @param mercatorLng 墨卡托x坐标 - * @param mercatorLat 墨卡托y坐标 - * @returns {{lng: number, lat: number}} WGS-84经纬度 - */ - function convertMercatorToLatLng(mercatorLng: number, mercatorLat: number): { lng: number, lat: number } { - const lng = (mercatorLng / 20037508.34) * 180; - let lat = (mercatorLat / 20037508.34) * 180; - lat = (180 / Math.PI) * (2 * Math.atan(Math.exp(lat * Math.PI / 180)) - Math.PI / 2); - // 此转换未处理BD-09到WGS-84的偏移,但对于消除大的距离计算错误已经足够 - return { lng: lng, lat: lat }; - } - - /** - * 智能路线规划 - * 高级封装函数,根据用户当前位置和目的地名称,提供导航信息 - */ - async function planRoute(params: { - destination: string; // 目的地名称 - city_name?: string; // 城市名称,辅助目的地查找 - }): Promise { - try { - // 步骤 1: 获取当前位置和搜索目的地(所有模式通用) - const currentLocation = await getCurrentLocation(); - if (!currentLocation) { - return { success: false, message: "无法获取当前位置信息" }; - } - - const searchResults = await search_aoi({ - keyword: params.destination, - city_name: params.city_name - }); - - if (!searchResults.success || !searchResults.aois || searchResults.aois.length === 0) { - return { - success: false, - message: `未能找到目的地: ${params.destination}`, - current_location: currentLocation - }; - } - - const destination = searchResults.aois[0]; - const destLocation = destination.center; - - if (!destLocation || !destLocation.lng || !destLocation.lat) { - return { - success: false, - message: `目的地坐标信息无效: ${params.destination}`, - current_location: currentLocation - }; - } - - const destLatLng = convertMercatorToLatLng(destLocation.lng, destLocation.lat); - logger(LOG_LEVELS.DEBUG, `目的地 "${destination.name}" 墨卡托坐标:`, destLocation); - logger(LOG_LEVELS.DEBUG, `转换后的经纬度:`, destLatLng); - - const distance = calculateDistance( - currentLocation.lat, - currentLocation.lng, - destLatLng.lat, - destLatLng.lng - ); - - // 帮助函数,用于计算特定交通方式的路线详情 - const getRouteDetailsForMode = async (mode: "driving" | "walking" | "transit"): Promise => { - let cityCode = "1"; - if (params.city_name) { - cityCode = await getCityCode(params.city_name); - } - const navUrl = `https://api.map.baidu.com/direction?origin=${currentLocation.lat},${currentLocation.lng}&destination=${destLatLng.lat},${destLatLng.lng}&mode=${mode}®ion=${cityCode}&output=html`; - - return { - estimated_distance: `${(distance / 1000).toFixed(2)}公里`, - estimated_duration: estimateDuration(distance, mode), - transport_mode: mode, - navigation_url: navUrl, - suggestion: getSuggestion(distance, mode) - }; - }; - - // 并发获取所有交通方式的路线 - logger(LOG_LEVELS.INFO, "获取所有模式的路线规划..."); - const modes: ("driving" | "walking" | "transit")[] = ["driving", "walking", "transit"]; - const allRoutesPromises = modes.map(mode => getRouteDetailsForMode(mode)); - const routesResults = await Promise.all(allRoutesPromises); - - const all_routes = { - driving: routesResults[0], - walking: routesResults[1], - transit: routesResults[2] - }; - - return { - success: true, - current_location: currentLocation, - destination: { - name: destination.name, - address: destination.address, - location: destLatLng - }, - all_routes: all_routes - }; - } catch (error) { - logger(LOG_LEVELS.ERROR, `[planRoute] 错误:`, error); - return { - success: false, - message: `路线规划失败: ${error.message}` - }; - } - } - - // 工具函数:计算两点间距离(米) - function calculateDistance(lat1: number, lng1: number, lat2: number, lng2: number): number { - const R = 6371000; // 地球半径,单位米 - const dLat = (lat2 - lat1) * Math.PI / 180; - const dLng = (lng2 - lng1) * Math.PI / 180; - const a = - Math.sin(dLat / 2) * Math.sin(dLat / 2) + - Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * - Math.sin(dLng / 2) * Math.sin(dLng / 2); - const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - return R * c; - } - - // 工具函数:格式化时间为可读字符串 - function formatDuration(minutes: number): string { - if (minutes < 60) { - return `约${Math.ceil(minutes)}分钟`; - } - const hours = Math.floor(minutes / 60); - const remainingMinutes = Math.ceil(minutes % 60); - if (remainingMinutes === 0) { - return `约${hours}小时`; - } - return `约${hours}小时${remainingMinutes}分钟`; - } - - // 工具函数:估算行程时间 - function estimateDuration(distance: number, mode: string): string { - let minutes = 0; - const distanceInKm = distance / 1000; - - switch (mode) { - case "walking": - // 步行速度约 4 km/h (15 min/km) - minutes = distanceInKm * 15; - break; - case "transit": - // 公共交通,分段估算 - if (distanceInKm < 10) { // 市内 - minutes = distanceInKm * 4; // 15 km/h - } else if (distanceInKm < 100) { - minutes = distanceInKm * 2; // 30 km/h - } else { // 长途 - minutes = distanceInKm * 1; // 60 km/h - } - break; - case "driving": - default: - // 驾车,分段估算 - if (distanceInKm < 10) { // 市区 - minutes = distanceInKm * 2; // 30 km/h - } else if (distanceInKm < 100) { - minutes = distanceInKm * 1.2; // 50 km/h - } else { // 高速 - minutes = distanceInKm * 0.75; // 80 km/h - } - break; - } - return formatDuration(minutes); - } - - // 工具函数:根据距离和交通方式给出建议 - function getSuggestion(distance: number, mode: string): string { - if (distance < 500) { - return "目的地非常近,步行即可到达"; - } else if (distance < 2000) { - return "距离适中,可步行或乘坐短途交通工具"; - } else if (distance < 5000) { - return "距离较远,建议使用公共交通工具"; - } else { - return "目的地较远,建议驾车或使用公共交通工具"; - } - } - - /** - * 创建HTTP客户端 - * 使用OkHttp库实现网络请求 - */ - function createHttpClient() { - return OkHttp.newBuilder() - .connectTimeout(10000) - .readTimeout(30000) - .writeTimeout(15000) - .followRedirects(true) - .build(); - } - - /** - * 获取城市编码 - * @param cityName 城市名称,如"杭州"、"南京"等 - * @returns 返回城市编码字符串,如果找不到返回默认编码"1"(全国) - */ - async function getCityCode(cityName: string): Promise { - try { - logger(LOG_LEVELS.INFO, `开始查询城市编码: ${cityName}`); - - // 如果已在本地映射表中,直接返回 - if (CITY_CODES[cityName]) { - logger(LOG_LEVELS.DEBUG, `本地映射表中找到城市"${cityName}"的编码:`, CITY_CODES[cityName]); - return CITY_CODES[cityName]; - } - - // 使用百度地图API查询城市信息 - const encodedCityName = encodeURIComponentSafe(cityName); - const url = `https://map.baidu.com/?newmap=1&qt=s&wd=${encodedCityName}&c=1`; - - logger(LOG_LEVELS.INFO, `发送请求获取城市编码:`, url); - const result = await httpGet(url) as BaiduMap.ApiResponse; - - // 尝试从不同路径提取城市编码 - let cityCode = "1"; // 默认值 - - // 路径1: current_city.code - if (result && result.current_city && result.current_city.code) { - cityCode = result.current_city.code.toString(); - logger(LOG_LEVELS.DEBUG, `从current_city中找到城市编码:`, cityCode); - } - // 路径2: content[].area_code 或 city_id - else if (result && result.content && Array.isArray(result.content) && result.content.length > 0) { - for (const item of result.content) { - if (item.area_code) { - cityCode = item.area_code.toString(); - logger(LOG_LEVELS.DEBUG, `从content[].area_code中找到城市编码:`, cityCode); - break; - } else if (item.city_id) { - cityCode = item.city_id.toString(); - logger(LOG_LEVELS.DEBUG, `从content[].city_id中找到城市编码:`, cityCode); - break; - } - } - } - // 路径3: result_code - else if (result && result.result && result.result.code) { - cityCode = result.result.code.toString(); - logger(LOG_LEVELS.DEBUG, `从result.code中找到城市编码:`, cityCode); - } - // 路径4: result.city_id - else if (result && result.result && result.result.city_id) { - cityCode = result.result.city_id.toString(); - logger(LOG_LEVELS.DEBUG, `从result.city_id中找到城市编码:`, cityCode); - } - - // 如果没找到,尝试第二个API端点 - if (cityCode === "1") { - // 使用城市搜索API - const secondUrl = `https://map.baidu.com/?qt=cur&wd=${encodedCityName}`; - logger(LOG_LEVELS.DEBUG, `未找到编码,尝试第二个API端点:`, secondUrl); - - try { - // 增加延迟避免请求过快 - await Tools.System.sleep(500); - - const secondResult = await httpGet(secondUrl); - if (secondResult && secondResult.current_city && secondResult.current_city.code) { - cityCode = secondResult.current_city.code.toString(); - logger(LOG_LEVELS.DEBUG, `从第二个API获取到城市编码:`, cityCode); - } - } catch (e) { - logger(LOG_LEVELS.ERROR, `第二个API请求失败:`, e); - } - } - - // 如果仍未找到,使用默认值 - if (cityCode === "1") { - logger(LOG_LEVELS.INFO, `未能找到城市"${cityName}"的编码,使用默认编码"1"(全国)`); - } else { - // 找到编码后,可以临时添加到CITY_CODES中供本次会话使用 - CITY_CODES[cityName] = cityCode; - logger(LOG_LEVELS.DEBUG, `已将城市"${cityName}"的编码${cityCode}添加到临时映射表`); - } - - return cityCode; - } catch (error) { - logger(LOG_LEVELS.ERROR, `获取城市编码失败:`, error); - return "1"; // 出错时返回默认编码(全国) - } - } - - return { - - // 格式化工具函数 - formatAoiResultAsText: wrap(async (param: any) => formatAoiResultAsText(await search_aoi(param))), - formatRouteResultAsText: wrap(async (param: any) => formatRouteResultAsText(await planRoute(param))), - - // 测试函数 - main: wrap(main), - }; -})(); - -// 格式化工具函数导出 -exports.search_aoi = baiduMap.formatAoiResultAsText; -exports.planRoute = baiduMap.formatRouteResultAsText; - -// 测试函数导出 -exports.main = baiduMap.main; \ No newline at end of file diff --git a/scripts/beeimg_image_uploader.ts b/scripts/beeimg_image_uploader.ts deleted file mode 100644 index 1973d7d..0000000 --- a/scripts/beeimg_image_uploader.ts +++ /dev/null @@ -1,190 +0,0 @@ -/* METADATA -{ - "name": "beeimg_image_uploader_v2", - - "display_name": { - "zh": "Beeimg 图片上传器 V2", - "en": "Beeimg Image Uploader V2" - }, - "description": { - "zh": "BeeIMG(https://beeimg.com/)工具将本地图片上传到图床并返回图片url,配合图片生成工具实现图生图(图生图务必开启)。", - "en": "Upload a local image to BeeIMG (https://beeimg.com/) and return a public image URL. Useful for image-to-image workflows (make sure img2img is enabled)." - }, - "env": [ - "BEEIMG_API_KEY" - ], - "category": "Media", - "tools": [ - { - "name": "upload_image", - "description": { - "zh": "使用 multipart 上传将本地图片上传到 BeeIMG 图床并返回图片 url。", - "en": "Upload a local image to BeeIMG via multipart upload and return the image URL." - }, - "parameters": [ - { "name": "file_path", "description": { "zh": "要上传的图片文件绝对路径 (建议使用 /sdcard/ 开头的完整路径)。", "en": "Absolute path of the image file to upload (recommended: full path starting with /sdcard/)." }, "type": "string", "required": true }, - { "name": "album_id", "description": { "zh": "相册ID (可选)。", "en": "Album ID (optional)." }, "type": "string", "required": false }, - { "name": "privacy", "description": { "zh": "隐私设置,'public' 或 'private' (可选)。", "en": "Privacy setting: 'public' or 'private' (optional)." }, "type": "string", "required": false } - ] - } - ] -}*/ - -const beeimgUploader = (function () { - // API配置 - const API_ENDPOINT = "https://beeimg.com/api/upload/file/json/"; - - function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null; - } - - function getErrorMessage(error: unknown): string { - if (error instanceof Error) return error.message; - return String(error); - } - - function getErrorStack(error: unknown): string | undefined { - if (error instanceof Error) return error.stack; - return undefined; - } - - function getApiKey(): string { - return getEnv("BEEIMG_API_KEY") || ""; - } - - function guessMimeTypeFromPath(filePath: string): string { - const lower = filePath.toLowerCase(); - if (lower.endsWith(".png")) return "image/png"; - if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg"; - if (lower.endsWith(".webp")) return "image/webp"; - if (lower.endsWith(".gif")) return "image/gif"; - return "application/octet-stream"; - } - - function safeJsonParseLoose(text: string): unknown { - const trimmed = (text || "").trim(); - if (!trimmed) return null; - try { - return JSON.parse(trimmed) as unknown; - } catch (e) { - const start = trimmed.indexOf("{"); - const end = trimmed.lastIndexOf("}"); - if (start !== -1 && end !== -1 && end > start) { - return JSON.parse(trimmed.substring(start, end + 1)) as unknown; - } - throw e; - } - } - - interface UploadImageParams { - file_path: string; - album_id?: string; - privacy?: string; - } - - interface UploadImageResult { - url: string; - thumbnail_url?: string; - page_url?: string; - details: string; - } - - async function upload_image(params: UploadImageParams): Promise { - const file_path = params?.file_path; - const album_id = params?.album_id; - const privacy = params?.privacy; - - if (!file_path || String(file_path).trim().length === 0) { - throw new Error("参数 file_path 不能为空。"); - } - - // 1. 检查文件是否存在 - const fileExists = await Tools.Files.exists(file_path); - if (!fileExists.exists) { - throw new Error(`文件未找到: ${file_path} (请尝试使用 /sdcard/ 开头的绝对路径)`); - } - - // 2. 准备参数 - const apiKey = getApiKey(); - if (!apiKey) { - throw new Error("BEEIMG_API_KEY 未配置,请在环境变量中设置 BeeIMG 的 API Key。"); - } - - const form_data: Record = { - apikey: apiKey - }; - if (album_id) form_data.albumid = String(album_id); - if (privacy) form_data.privacy = String(privacy); - - // 3. 发起 multipart 上传 - console.log("正在执行上传..."); - - const resp = await Tools.Net.uploadFile({ - url: API_ENDPOINT, - method: "POST", - headers: { - // 尽量模拟浏览器 UA,降低被风控概率 - "User-Agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36" - }, - form_data, - files: [ - { - field_name: "file", - file_path: file_path, - content_type: guessMimeTypeFromPath(file_path) - } - ] - }); - - if (resp.statusCode < 200 || resp.statusCode >= 300) { - throw new Error(`上传请求失败 (HTTP ${resp.statusCode})。可能是网络问题或 IP 被屏蔽。\n响应: ${resp.content}`); - } - - const responseText = (resp.content || "").trim(); - if (!responseText) { - throw new Error("上传失败:服务器返回了空内容。请检查网络连接或尝试关闭 VPN。"); - } - - let jsonResponse: unknown; - try { - jsonResponse = safeJsonParseLoose(responseText); - } catch (e: unknown) { - throw new Error(`API 响应解析失败。服务器返回内容不是 JSON:\n${responseText.substring(0, 200)}...`); - } - - const files = isRecord(jsonResponse) && isRecord(jsonResponse["files"]) ? (jsonResponse["files"] as Record) : null; - const ok = !!files && (files["status"] === "Success" || files["code"] === "200" || files["code"] === 200); - const url = files ? files["url"] : undefined; - - if (ok && (typeof url === "string" || typeof url === "number") && String(url).trim().length > 0) { - return { - url: String(url), - thumbnail_url: files && files["thumbnail_url"] ? String(files["thumbnail_url"]) : undefined, - page_url: files && files["view_url"] ? String(files["view_url"]) : undefined, - details: `上传成功! URL: ${String(url)}` - }; - } else { - const errMsg = files - ? (files["status"] || files["message"] || JSON.stringify(files)) - : "未知错误"; - throw new Error(`BeeIMG API 报错: ${errMsg}`); - } - } - - async function wrap(func: (params: TParams) => Promise, params: TParams) { - try { - const result = await func(params); - complete({ success: true, message: "图片上传成功", data: result }); - } - catch (error: unknown) { - console.error(`Error: ${getErrorMessage(error)}`); - complete({ success: false, message: getErrorMessage(error), error_stack: getErrorStack(error) }); - } - } - - return { - upload_image: (p: UploadImageParams) => wrap(upload_image, p) - }; -})(); - -exports.upload_image = beeimgUploader.upload_image; diff --git a/scripts/bilibili_tools.ts b/scripts/bilibili_tools.ts deleted file mode 100644 index 5fa2447..0000000 --- a/scripts/bilibili_tools.ts +++ /dev/null @@ -1,768 +0,0 @@ -/* METADATA -{ - "name": "bilibili_tools", - "display_name": { - "zh": "Bilibili 工具集", - "en": "Bilibili Toolkit" - }, - "description": { "zh": "提供B站视频信息分析功能,包括获取字幕、弹幕、评论和搜索视频。", "en": "Analyze Bilibili video information, including subtitles, danmaku, comments, and video search." }, - "enabledByDefault": true, - "category": "Media", - "tools": [ - { - "name": "get_subtitles", - "description": { "zh": "从Bilibili视频中获取字幕。", "en": "Get subtitles from a Bilibili video." }, - "parameters": [ - { "name": "url", "description": { "zh": "Bilibili视频URL,例如:https://www.bilibili.com/video/BV1x341177NN", "en": "Bilibili video URL, e.g. https://www.bilibili.com/video/BV1x341177NN" }, "type": "string", "required": true } - ] - }, - { - "name": "get_danmaku", - "description": { "zh": "从Bilibili视频中获取弹幕。", "en": "Get danmaku (bullet comments) from a Bilibili video." }, - "parameters": [ - { "name": "url", "description": { "zh": "Bilibili视频URL,例如:https://www.bilibili.com/video/BV1x341177NN", "en": "Bilibili video URL, e.g. https://www.bilibili.com/video/BV1x341177NN" }, "type": "string", "required": true }, - { "name": "count", "description": { "zh": "要获取的弹幕数量,默认500,最多1000", "en": "Number of danmaku items to fetch (default: 500, max: 1000)." }, "type": "number", "required": false } - ] - }, - { - "name": "get_comments", - "description": { "zh": "从Bilibili视频中获取热门评论。", "en": "Get hot comments from a Bilibili video." }, - "parameters": [ - { "name": "url", "description": { "zh": "Bilibili视频URL,例如:https://www.bilibili.com/video/BV1x341177NN", "en": "Bilibili video URL, e.g. https://www.bilibili.com/video/BV1x341177NN" }, "type": "string", "required": true } - ] - }, - { - "name": "search_videos", - "description": { "zh": "在Bilibili上搜索视频。", "en": "Search videos on Bilibili." }, - "parameters": [ - { "name": "keyword", "description": { "zh": "要搜索的关键词", "en": "Keyword to search for." }, "type": "string", "required": true }, - { "name": "page", "description": { "zh": "页码,默认为1", "en": "Page number (default: 1)." }, "type": "number", "required": false }, - { "name": "count", "description": { "zh": "返回结果的数量,默认10,最多20", "en": "Number of results to return (default: 10, max: 20)." }, "type": "number", "required": false } - ] - }, - { - "name": "get_user_info", - "description": { "zh": "获取B站用户的基本信息。", "en": "Get basic information for a Bilibili user." }, - "parameters": [ - { "name": "mid", "description": { "zh": "用户的数字ID (UID)", "en": "User numeric ID (UID)." }, "type": "number", "required": true } - ] - } - ] -}*/ - -//参考了https://github.com/vruses/bili-api-interceptor/blob/master/dist/bili-api-interceptor.user.js 特此感谢 - -const BilibiliVideoAnalysis = (function () { - - interface ToolResponse { - success: boolean; - message: string; - data?: any; - } - - interface Subtitle { - lan: string; - content: string[]; - } - - interface Comment { - user: string; - content: string; - likes: number; - time: string; // 添加评论时间 - sub_comments: Comment[]; // 添加子评论列表 - } - - interface SearchResultVideo { - title: string; - author: string; - bvid: string; - play: number; - danmaku: number; - like: number; - duration: string; - pubdate: number; - description: string; - } - - interface UserInfo { - name: string; - sex: string; - face: string; - sign: string; - level: number; - birthday: string; - follower: number; - following: number; - mid: number; - } - - // Bilibili API endpoints - const API_GET_VIEW_INFO = "https://api.bilibili.com/x/web-interface/view"; - const API_GET_SUBTITLE_LIST = "https://api.bilibili.com/x/player/v2"; - const API_GET_DANMAKU = "https://api.bilibili.com/x/v1/dm/list.so"; - const API_GET_COMMENTS = "https://api.bilibili.com/x/v2/reply/wbi/main"; - const API_SEARCH = "https://api.bilibili.com/x/web-interface/search/all/v2"; - const API_NAV = "https://api.bilibili.com/x/web-interface/nav"; - const API_GET_USER_INFO = "https://api.bilibili.com/x/space/wbi/acc/info"; - const API_GET_RELATION_STAT = "https://api.bilibili.com/x/relation/stat"; - - const client = OkHttp.newClient(); - let SESSDATA: string | undefined; - let wbiKeys: { img_key: string; sub_key: string; } | null = null; - let buvid3: string | null = null; - - function generateBuvid3(): string { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { - const r = Math.random() * 16 | 0; - const v = c === 'x' ? r : (r & 0x3 | 0x8); - return v.toString(16); - }); - } - - async function init() { - if (SESSDATA === undefined) { - // SESSDATA 是B站的Cookie,用于访问一些需要登录的API。 - // 为了简单起见,这里我们不作强制要求。 - // 如果遇到请求失败的情况,可以尝试手动在这里设置你的SESSDATA值。 - // 例如: SESSDATA = "your_sessdata_cookie_value"; - SESSDATA = ""; - if (!SESSDATA) { - console.log("SESSDATA 未设置,部分需要登录的请求可能会失败。"); - } - } - // Pre-fetch WBI keys if not already available. - if (!wbiKeys) { - wbiKeys = await get_wbi_keys(); - } - } - - function getHeaders(): Record { - const headers: Record = { - 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36', - 'Referer': 'https://www.bilibili.com/' - }; - - if (!buvid3) { - buvid3 = generateBuvid3(); - } - - const cookies = [`buvid3=${buvid3}`]; - if (SESSDATA) { - cookies.push(`SESSDATA=${SESSDATA}`); - } - headers['Cookie'] = cookies.join('; '); - - return headers; - } - - function unescapeXml(text: string): string { - return text.replace(/</g, '<') - .replace(/>/g, '>') - .replace(/&/g, '&') - .replace(/"/g, '"') - .replace(/'/g, "'"); - } - - const mixinKeyEncTab = [ - 46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, - 27, 43, 5, 49, 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, - 37, 48, 7, 16, 24, 55, 40, 61, 26, 17, 0, 1, 60, 51, 30, 4, - 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11, 36, 20, 34, 44, 52 - ]; - - function getMixinKey(orig: string): string { - return mixinKeyEncTab.map(n => orig[n]).join('').slice(0, 32); - } - - function encWbi(params: Record, img_key: string, sub_key: string): string { - const mixin_key = getMixinKey(img_key + sub_key); - const curr_time = Math.round(Date.now() / 1000); - const chr_filter = /[!'()*]/g; - - const new_params = { ...params, wts: curr_time }; - - const query = Object.keys(new_params).sort().map(key => { - const value = String(new_params[key]).replace(chr_filter, ""); - return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`; - }).join('&'); - - const w_rid = CryptoJS.MD5(query + mixin_key).toString(); - return `${query}&w_rid=${w_rid}`; - } - - async function get_wbi_keys(): Promise<{ img_key: string, sub_key: string } | null> { - try { - const response = await client.get(API_NAV, getHeaders()); - if (!response.isSuccessful()) { - console.error("Failed to get WBI keys, status: " + response.statusCode); - return null; - } - const navData = response.json(); - - // The API may return a non-zero code for non-logged-in users, but still provide the WBI keys. - if (!navData.data?.wbi_img?.img_url || !navData.data?.wbi_img?.sub_url) { - console.error("Failed to get WBI keys: " + (navData.message || "No wbi_img in response")); - return null; - } - - const imgUrl = navData.data.wbi_img.img_url; - const subUrl = navData.data.wbi_img.sub_url; - - const img_key = imgUrl.substring(imgUrl.lastIndexOf('/') + 1, imgUrl.lastIndexOf('.')); - const sub_key = subUrl.substring(subUrl.lastIndexOf('/') + 1, subUrl.lastIndexOf('.')); - - return { img_key, sub_key }; - } catch (e: any) { - console.error(`Error fetching WBI keys: ${e.message}`); - return null; - } - } - - async function extract_bvid(url: string): Promise { - let match = url.match(/BV[a-zA-Z0-9_]+/); - if (match) { - return match[0]; - } - - if (url.includes('b23.tv')) { - try { - const request = client.newRequest().url(url).method("HEAD").build(); - const response = await request.execute(); - const finalUrl = response.raw.url; - if (finalUrl) { - match = finalUrl.match(/BV[a-zA-Z0-9_]+/); - if (match) { - return match[0]; - } - } - } catch (e) { - console.error(`Error resolving short URL: ${e}`); - } - } - return null; - } - - async function get_video_basic_info(bvid: string): Promise<{ aid: number | null, cid: number | null, error: string | null }> { - try { - const url = `${API_GET_VIEW_INFO}?bvid=${bvid}`; - const response = await client.get(url, getHeaders()); - if (!response.isSuccessful()) { - return { aid: null, cid: null, error: `Failed to get video info, status: ${response.statusCode}` }; - } - const data = response.json(); - if (data.code !== 0) { - return { aid: null, cid: null, error: `Failed to get video info: ${data.message}` }; - } - const videoData = data.data; - return { aid: videoData.aid, cid: videoData.cid, error: null }; - } catch (e: any) { - return { aid: null, cid: null, error: `Failed to fetch video details: ${e.message}` }; - } - } - - async function get_subtitles_from_api(aid: number, cid: number): Promise<{ subtitles: Subtitle[], error: string | null }> { - const subtitles: Subtitle[] = []; - try { - const url = `${API_GET_SUBTITLE_LIST}?aid=${aid}&cid=${cid}`; - const response = await client.get(url, getHeaders()); - if (!response.isSuccessful()) { - return { subtitles: [], error: `Could not fetch subtitles list, status: ${response.statusCode}` }; - } - const subtitleListData = response.json(); - if (subtitleListData.code === 0 && subtitleListData.data?.subtitle?.subtitles) { - for (const sub_meta of subtitleListData.data.subtitle.subtitles) { - if (sub_meta.subtitle_url) { - try { - const subtitle_json_url = `https:${sub_meta.subtitle_url}`; - const sub_content_response = await client.get(subtitle_json_url, getHeaders()); - if (sub_content_response.isSuccessful()) { - const sub_content = await sub_content_response.json(); - const subtitle_body = sub_content.body || []; - const content_list = subtitle_body.map((item: any) => item.content || ''); - subtitles.push({ - lan: sub_meta.lan, - content: content_list - }); - } - } catch (e: any) { - console.error(`Could not fetch or parse subtitle content from ${sub_meta.subtitle_url}: ${e.message}`); - } - } - } - } - return { subtitles, error: null }; - } catch (e: any) { - return { subtitles: [], error: `Could not fetch subtitles: ${e.message}` }; - } - } - - async function get_danmaku_from_api(cid: number, count: number): Promise<{ danmaku: string[], error: string | null }> { - const danmaku_list: string[] = []; - try { - const url = `${API_GET_DANMAKU}?oid=${cid}`; - const response = await client.get(url, getHeaders()); - - if (!response.isSuccessful()) { - return { danmaku: [], error: `Failed to get danmaku, status: ${response.statusCode}` }; - } - - const compressedData = response.bodyAsBase64(); - console.log("base64" + compressedData); - if (!compressedData) { - return { danmaku: [], error: null }; - } - - const danmaku_content = pako.inflate(compressedData, { to: 'string' }); - - const regex = /(.*?)<\/d>/g; - let match; - while ((match = regex.exec(danmaku_content)) !== null) { - if (danmaku_list.length >= count) { - break; - } - danmaku_list.push(unescapeXml(match[1])); - } - - return { danmaku: danmaku_list, error: null }; - } catch (e: any) { - return { danmaku: [], error: `Failed to get or parse danmaku: ${e.message}` }; - } - } - - async function get_comments_from_api(aid: number): Promise<{ comments: string, count: number, error: string | null }> { - const all_comments: Comment[] = []; - - if (!wbiKeys) { - return { comments: "", count: 0, error: "获取 WBI keys 失败,无法请求评论 API" }; - } - - let page_num = 1; - let total_pages = 1; - - try { - while (page_num <= total_pages) { - const params = { - type: 1, - oid: aid, - sort: 2, // sort=2 for hot - pn: page_num - }; - - const signed_query = encWbi(params, wbiKeys.img_key, wbiKeys.sub_key); - const url = `${API_GET_COMMENTS}?${signed_query}`; - - const response = await client.get(url, getHeaders()); - - if (!response.isSuccessful()) { - console.error(`Failed to get comments page ${page_num}, status: ${response.statusCode}`); - page_num++; - continue; - } - - const comments_data = response.json(); - - if (comments_data.code !== 0) { - console.error(`API error on page ${page_num}: ${comments_data.message}`); - page_num++; - continue; - } - - if (page_num === 1 && comments_data.data?.page) { - total_pages = Math.ceil(comments_data.data.page.count / comments_data.data.page.size); - } - - if (comments_data.data?.replies) { - for (const comment of comments_data.data.replies) { - const formatted_comment = format_comment(comment); - if (formatted_comment) { - all_comments.push(formatted_comment); - } - } - } - page_num++; - } - const comment_summary = format_comments_to_string(all_comments); - return { comments: comment_summary, count: all_comments.length, error: null }; - } catch (e: any) { - const comment_summary = format_comments_to_string(all_comments); - return { comments: comment_summary, count: all_comments.length, error: `Failed to get comments: ${e.message}` }; // 返回已获取的部分数据 - } - } - - function format_comment(comment_item: any): Comment { - const sub_comments = (comment_item.replies || []).map(format_comment).filter(Boolean); - return { - user: comment_item.member.uname, - content: comment_item.content.message, - likes: comment_item.like || 0, - time: new Date(comment_item.ctime * 1000).toLocaleDateString(), - sub_comments: sub_comments - }; - } - - function format_comments_to_string(comments: Comment[]): string { - const result: string[] = []; - function format_single(comment: Comment, indent: string) { - result.push(`${indent}- ${comment.user} (👍${comment.likes}) [${comment.time}]: ${comment.content}`); - if (comment.sub_comments && comment.sub_comments.length > 0) { - for (const sub of comment.sub_comments) { - format_single(sub, indent + " "); - } - } - } - for (const comment of comments) { - format_single(comment, ""); - } - return result.join('\n'); - } - - - async function get_subtitles(params: { url: string }): Promise { - await init(); - const bvid = await extract_bvid(params.url); - if (!bvid) { - return { success: false, message: `错误: 无法从 URL 提取 BV 号: ${params.url}` }; - } - - const { aid, cid, error: infoError } = await get_video_basic_info(bvid); - if (infoError) { - return { success: false, message: `获取视频信息失败: ${infoError}` }; - } - if (!aid || !cid) { - return { success: false, message: '获取视频 aid 或 cid 失败' }; - } - - const { subtitles, error: subtitlesError } = await get_subtitles_from_api(aid, cid); - if (subtitlesError) { - return { success: false, message: `获取字幕失败: ${subtitlesError}` }; - } - - if (subtitles.length === 0) { - return { success: true, message: "该视频没有字幕", data: [] }; - } - - return { success: true, message: `成功获取 ${subtitles.length} 门语言的字幕`, data: subtitles }; - } - - async function get_danmaku(params: { url: string, count?: number }): Promise { - await init(); - const { url, count = 500 } = params; - const bvid = await extract_bvid(url); - if (!bvid) { - return { success: false, message: `错误: 无法从 URL 提取 BV 号: ${url}` }; - } - - if (count > 1000) { - return { success: false, message: "参数 'count' 不能超过 1000" }; - } - - const { aid, cid, error: infoError } = await get_video_basic_info(bvid); - if (infoError) { - return { success: false, message: `获取视频信息失败: ${infoError}` }; - } - if (!cid) { - return { success: false, message: '获取视频 cid 失败' }; - } - - const { danmaku, error: danmakuError } = await get_danmaku_from_api(cid, count); - if (danmakuError) { - return { success: false, message: `获取弹幕失败: ${danmakuError}` }; - } - - if (danmaku.length === 0) { - return { success: true, message: "该视频没有弹幕", data: [] }; - } - - return { success: true, message: `成功获取 ${danmaku.length} 条弹幕`, data: danmaku }; - } - - async function get_comments(params: { url: string }): Promise { - await init(); - const bvid = await extract_bvid(params.url); - if (!bvid) { - return { success: false, message: `错误: 无法从 URL 提取 BV 号: ${params.url}` }; - } - - const { aid, error: infoError } = await get_video_basic_info(bvid); - if (infoError) { - return { success: false, message: `获取视频信息失败: ${infoError}` }; - } - if (!aid) { - return { success: false, message: '获取视频 aid 失败' }; - } - - const { comments, count, error: commentsError } = await get_comments_from_api(aid); - - if (commentsError && count === 0) { - return { success: false, message: `获取评论失败: ${commentsError}` }; - } - - if (count === 0) { - return { success: true, message: "该视频没有热门评论", data: "" }; - } - - const message = commentsError - ? `成功获取 ${count} 条热门评论,但过程中发生错误: ${commentsError}` - : `成功获取 ${count} 条热门评论`; - - return { success: true, message: message, data: comments }; - } - - async function search_videos_from_api(keyword: string, page: number): Promise<{ results: any, error: string | null }> { - if (!wbiKeys) { - return { results: null, error: "获取 WBI keys 失败,无法请求搜索 API" }; - } - - try { - const params = { - keyword: keyword, - page: page, - search_type: "video" - }; - - const signed_query = encWbi(params, wbiKeys.img_key, wbiKeys.sub_key); - const url = `${API_SEARCH}?${signed_query}`; - - const response = await client.get(url, getHeaders()); - - if (!response.isSuccessful()) { - return { results: null, error: `搜索失败, status: ${response.statusCode}` }; - } - - const search_data = response.json(); - if (search_data.code !== 0) { - return { results: null, error: `搜索 API 错误: ${search_data.message}` }; - } - - return { results: search_data.data, error: null }; - } catch (e: any) { - return { results: null, error: `搜索时发生异常: ${e.message}` }; - } - } - - function format_search_results_to_string(data: any, count: number): { formatted: string, result_count: number, total_count: number } { - if (!data.result) { - return { formatted: "没有找到相关结果。", result_count: 0, total_count: 0 }; - } - - const videoResults = data.result - .find((item: any) => item.result_type === "video") - ?.data; - - if (!videoResults || videoResults.length === 0) { - return { formatted: "没有找到相关视频结果。", result_count: 0, total_count: data.numResults || 0 }; - } - - const slicedResults = videoResults.slice(0, count); - - const formattedResults = slicedResults - .map((video: SearchResultVideo, index: number) => { - const cleanTitle = video.title.replace(/|<\/em>/g, ""); - const cleanDescription = video.description.replace(/|<\/em>/g, ""); - return [ - `${index + 1}. "${cleanTitle}" - ${video.author}`, - ` BV ID: ${video.bvid}`, - ` 播放: ${video.play?.toLocaleString()}`, - ` 弹幕: ${video.danmaku?.toLocaleString()}`, - ` 点赞: ${video.like?.toLocaleString()}`, - ` 时长: ${video.duration}`, - ` 发布于: ${new Date(video.pubdate * 1000).toLocaleDateString()}`, - ` 简介: ${cleanDescription?.substring(0, 100)}${cleanDescription?.length > 100 ? "..." : ""}`, - ].join("\n"); - }) - .join("\n\n"); - - return { formatted: formattedResults, result_count: slicedResults.length, total_count: data.numResults || 0 }; - } - - - async function search_videos(params: { keyword: string, page?: number, count?: number }): Promise { - await init(); - const { keyword, page = 1, count = 10 } = params; - - if (count > 20) { - return { success: false, message: "参数 'count' 不能超过 20" }; - } - - const { results, error } = await search_videos_from_api(keyword, page); - if (error) { - return { success: false, message: `搜索失败: ${error}` }; - } - - const { formatted, result_count, total_count } = format_search_results_to_string(results, count); - - if (result_count === 0) { - return { success: true, message: "没有找到相关视频。", data: "" }; - } - - const message = `成功为 "${keyword}" 找到 ${total_count} 个相关视频,当前显示第 ${page} 页的 ${result_count} 个结果。`; - return { success: true, message, data: formatted }; - } - - function format_user_info_to_string(user: UserInfo): string { - return [ - `昵称: ${user.name} (UID: ${user.mid})`, - `性别: ${user.sex}`, - `等级: LV${user.level}`, - `生日: ${user.birthday || '未设置'}`, - `粉丝数: ${user.follower.toLocaleString()}`, - `关注数: ${user.following.toLocaleString()}`, - `个人简介: ${user.sign || '这个UP主很懒,什么都没有写...'}`, - `头像链接: ${user.face}` - ].join('\n'); - } - - async function get_user_info_from_api(mid: number): Promise<{ user_info: UserInfo | null, error: string | null }> { - if (!wbiKeys) { - return { user_info: null, error: "获取 WBI keys 失败,无法请求用户信息 API" }; - } - - try { - // Get user account info (WBI signed) - const params = { mid }; - const signed_query = encWbi(params, wbiKeys.img_key, wbiKeys.sub_key); - const url = `${API_GET_USER_INFO}?${signed_query}`; - const response = await client.get(url, getHeaders()); - - if (!response.isSuccessful()) { - return { user_info: null, error: `获取用户信息失败, status: ${response.statusCode}` }; - } - - const userData = response.json(); - if (userData.code !== 0) { - return { user_info: null, error: `用户信息 API 错误: ${userData.message}` }; - } - - // Get user relation stat (not WBI signed) - const statUrl = `${API_GET_RELATION_STAT}?vmid=${mid}`; - const statResponse = await client.get(statUrl, getHeaders()); - let follower = -1, following = -1; - if (statResponse.isSuccessful()) { - const statData = statResponse.json(); - if (statData.code === 0) { - follower = statData.data.follower; - following = statData.data.following; - } - } - - const userInfo: UserInfo = { - mid: userData.data.mid, - name: userData.data.name, - sex: userData.data.sex, - face: userData.data.face, - sign: userData.data.sign, - level: userData.data.level, - birthday: userData.data.birthday, - follower: follower, - following: following - }; - - return { user_info: userInfo, error: null }; - } catch (e: any) { - return { user_info: null, error: `获取用户信息时发生异常: ${e.message}` }; - } - } - - async function get_user_info(params: { mid: number }): Promise { - await init(); - const { mid } = params; - - if (!mid || typeof mid !== 'number' || mid <= 0) { - return { success: false, message: "参数 'mid' 必须是一个正整数" }; - } - - const { user_info, error } = await get_user_info_from_api(mid); - if (error) { - return { success: false, message: `获取用户信息失败: ${error}` }; - } - - if (!user_info) { - return { success: false, message: "未能获取到用户信息" }; - } - - const formatted_info = format_user_info_to_string(user_info); - const message = `成功获取 UID:${mid} 的用户信息。`; - - return { success: true, message, data: formatted_info }; - } - - async function wrapToolExecution

(func: (params: P) => Promise, params: P) { - try { - const result = await func(params); - complete(result); - } catch (error: any) { - console.error(`Tool ${func.name} failed unexpectedly`, error); - complete({ - success: false, - message: `工具执行时发生意外错误: ${error.message}`, - }); - } - } - - async function main() { - console.log("--- Bilibili Video Analysis Tool Test ---"); - const testUrl = "https://www.bilibili.com/video/BV1NRE5zmE3W"; - const testKeyword = "OpenAI"; - - console.log("\n[1/4] Testing search_videos..."); - const searchResult = await search_videos({ keyword: testKeyword, count: 10 }); - if (searchResult.success) { - console.log(`Success: ${searchResult.message}`); - console.log(`---------- Search Results for "${testKeyword}" ----------`); - console.log(searchResult.data); - console.log("------------------------------------------"); - } else { - console.error(`Failure: ${searchResult.message}`); - } - - console.log("\n[2/4] Testing get_subtitles..."); - const subtitlesResult = await get_subtitles({ url: testUrl }); - console.log(JSON.stringify(subtitlesResult, null, 2)); - - console.log("\n[3/4] Testing get_danmaku with count limit..."); - const danmakuResult = await get_danmaku({ url: testUrl, count: 20 }); - console.log(JSON.stringify(danmakuResult, null, 2)); - - console.log("\n[4/4] Testing get_comments..."); - const commentsResult = await get_comments({ url: testUrl }); - // For compact string format, we don't need JSON.stringify and can print directly - if (commentsResult.success) { - console.log(`Success: ${commentsResult.message}`); - console.log("---------- Comments ----------"); - console.log(commentsResult.data); - console.log("----------------------------"); - } else { - console.error(`Failure: ${commentsResult.message}`); - } - - console.log("\n[5/5] Testing get_user_info..."); - const userInfoResult = await get_user_info({ mid: 2 }); // 2 is B站创始人之一的UID - if (userInfoResult.success) { - console.log(`Success: ${userInfoResult.message}`); - console.log("---------- User Info ----------"); - console.log(userInfoResult.data); - console.log("-----------------------------"); - } else { - console.error(`Failure: ${userInfoResult.message}`); - } - - complete({ success: true, message: "Test finished." }); - } - - return { - get_subtitles: (params: { url: string }) => wrapToolExecution(get_subtitles, params), - get_danmaku: (params: { url: string, count?: number }) => wrapToolExecution(get_danmaku, params), - get_comments: (params: { url: string }) => wrapToolExecution(get_comments, params), - search_videos: (params: { keyword: string, page?: number, count?: number }) => wrapToolExecution(search_videos, params), - get_user_info: (params: { mid: number }) => wrapToolExecution(get_user_info, params), - main, - }; -})(); - -exports.get_subtitles = BilibiliVideoAnalysis.get_subtitles; -exports.get_danmaku = BilibiliVideoAnalysis.get_danmaku; -exports.get_comments = BilibiliVideoAnalysis.get_comments; -exports.search_videos = BilibiliVideoAnalysis.search_videos; -exports.get_user_info = BilibiliVideoAnalysis.get_user_info; -exports.main = BilibiliVideoAnalysis.main; \ No newline at end of file diff --git a/scripts/browser.ts b/scripts/browser.ts deleted file mode 100644 index 7eae59b..0000000 --- a/scripts/browser.ts +++ /dev/null @@ -1,509 +0,0 @@ -/* METADATA -{ - "name": "browser", - "display_name": { - "zh": "Browser 自动化操作", - "en": "Browser Automation" - }, - "description": { - "zh": "严格对齐 Playwright MCP 默认 browser 工具面的浏览器自动化工具集。", - "en": "Browser automation tools aligned to the default Playwright MCP browser surface." - }, - "enabledByDefault": true, - "category": "Automatic", - "tools": [ - { - "name": "click", - "description": { "zh": "按快照 ref 点击页面元素,包括同源 iframe 内的元素。", "en": "Click an element by snapshot ref, including refs inside same-origin iframes." }, - "parameters": [ - { "name": "ref", "description": { "zh": "快照中的目标元素引用;和 selector 至少提供一个。", "en": "Target element ref from the snapshot; provide ref or selector." }, "type": "string", "required": false }, - { "name": "selector", "description": { "zh": "可选,ref 不可用时的元素选择器。", "en": "Optional selector fallback when ref is not available." }, "type": "string", "required": false }, - { "name": "element", "description": { "zh": "可选,人类可读元素描述。", "en": "Optional human-readable element description." }, "type": "string", "required": false }, - { "name": "doubleClick", "description": { "zh": "可选,是否双击。", "en": "Optional double click." }, "type": "boolean", "required": false }, - { "name": "button", "description": { "zh": "可选,left/right/middle。", "en": "Optional mouse button: left/right/middle." }, "type": "string", "required": false }, - { "name": "modifiers", "description": { "zh": "可选,修饰键数组。", "en": "Optional modifier keys array." }, "type": "array", "required": false } - ] - }, - { - "name": "close", - "description": { "zh": "关闭当前 tab。", "en": "Close the current tab." }, - "parameters": [] - }, - { - "name": "console_messages", - "description": { "zh": "读取控制台消息。", "en": "Read console messages." }, - "parameters": [ - { "name": "level", "description": { "zh": "可选,日志级别:error/warning/info/debug,默认 info。", "en": "Optional log level: error/warning/info/debug. Defaults to info." }, "type": "string", "required": false }, - { "name": "filename", "description": { "zh": "可选,保存输出的文件名。", "en": "Optional output file name." }, "type": "string", "required": false } - ] - }, - { - "name": "drag", - "description": { "zh": "在两个元素之间拖拽。", "en": "Drag between two elements." }, - "parameters": [ - { "name": "startElement", "description": { "zh": "源元素的人类可读描述。", "en": "Human-readable source element description." }, "type": "string", "required": true }, - { "name": "startRef", "description": { "zh": "源元素 ref。", "en": "Source element ref." }, "type": "string", "required": true }, - { "name": "endElement", "description": { "zh": "目标元素的人类可读描述。", "en": "Human-readable target element description." }, "type": "string", "required": true }, - { "name": "endRef", "description": { "zh": "目标元素 ref。", "en": "Target element ref." }, "type": "string", "required": true } - ] - }, - { - "name": "evaluate", - "description": { "zh": "在页面或元素上执行 JavaScript 函数。", "en": "Evaluate a JavaScript function on the page or an element." }, - "parameters": [ - { "name": "function", "description": { "zh": "要执行的函数源码。", "en": "Function source to execute." }, "type": "string", "required": true }, - { "name": "element", "description": { "zh": "可选,人类可读元素描述。", "en": "Optional human-readable element description." }, "type": "string", "required": false }, - { "name": "ref", "description": { "zh": "可选,目标元素 ref。", "en": "Optional target element ref." }, "type": "string", "required": false } - ] - }, - { - "name": "upload", - "description": { "zh": "向当前文件选择器上传文件。", "en": "Upload files to the current file chooser." }, - "parameters": [ - { "name": "paths", "description": { "zh": "可选,绝对路径数组;不传则取消 file chooser。", "en": "Optional absolute file paths; omit to cancel the file chooser." }, "type": "array", "required": false } - ] - }, - { - "name": "fill_form", - "description": { "zh": "批量填写表单字段。", "en": "Fill multiple form fields." }, - "parameters": [ - { "name": "fields", "description": { "zh": "字段数组。", "en": "Array of form fields." }, "type": "array", "required": true } - ] - }, - { - "name": "handle_dialog", - "description": { "zh": "处理当前对话框。", "en": "Handle the current dialog." }, - "parameters": [ - { "name": "accept", "description": { "zh": "是否接受对话框。", "en": "Whether to accept the dialog." }, "type": "boolean", "required": true }, - { "name": "promptText", "description": { "zh": "可选,prompt 的输入文本。", "en": "Optional prompt text." }, "type": "string", "required": false } - ] - }, - { - "name": "hover", - "description": { "zh": "悬停到页面元素上。", "en": "Hover over an element." }, - "parameters": [ - { "name": "ref", "description": { "zh": "目标元素 ref。", "en": "Target element ref." }, "type": "string", "required": true }, - { "name": "element", "description": { "zh": "可选,人类可读元素描述。", "en": "Optional human-readable element description." }, "type": "string", "required": false } - ] - }, - { - "name": "goto", - "description": { "zh": "导航到指定 URL。", "en": "Navigate to a URL." }, - "parameters": [ - { "name": "url", "description": { "zh": "目标 URL。", "en": "Target URL." }, "type": "string", "required": true } - ] - }, - { - "name": "back", - "description": { "zh": "后退到上一页。", "en": "Go back to the previous page." }, - "parameters": [] - }, - { - "name": "network_requests", - "description": { "zh": "读取当前页面的网络请求。", "en": "Read network requests for the current page." }, - "parameters": [ - { "name": "includeStatic", "description": { "zh": "可选,是否包含静态资源请求,默认 false。", "en": "Optional include static resource requests. Defaults to false." }, "type": "boolean", "required": false }, - { "name": "filename", "description": { "zh": "可选,保存输出的文件名。", "en": "Optional output file name." }, "type": "string", "required": false } - ] - }, - { - "name": "press_key", - "description": { "zh": "按下键盘按键。", "en": "Press a keyboard key." }, - "parameters": [ - { "name": "key", "description": { "zh": "按键名。", "en": "Key name." }, "type": "string", "required": true } - ] - }, - { - "name": "resize", - "description": { "zh": "调整浏览器视口大小。", "en": "Resize the browser viewport." }, - "parameters": [ - { "name": "width", "description": { "zh": "宽度。", "en": "Width." }, "type": "number", "required": true }, - { "name": "height", "description": { "zh": "高度。", "en": "Height." }, "type": "number", "required": true } - ] - }, - { - "name": "run_code", - "description": { "zh": "运行 Playwright 风格代码片段。", "en": "Run a Playwright-style code snippet." }, - "parameters": [ - { "name": "code", "description": { "zh": "代码片段。", "en": "Code snippet." }, "type": "string", "required": true } - ] - }, - { - "name": "select_option", - "description": { "zh": "在下拉框中选择选项。", "en": "Select options in a dropdown." }, - "parameters": [ - { "name": "ref", "description": { "zh": "目标元素 ref。", "en": "Target element ref." }, "type": "string", "required": true }, - { "name": "values", "description": { "zh": "要选择的值数组。", "en": "Values to select." }, "type": "array", "required": true }, - { "name": "element", "description": { "zh": "可选,人类可读元素描述。", "en": "Optional human-readable element description." }, "type": "string", "required": false } - ] - }, - { - "name": "snapshot", - "description": { "zh": "获取当前页面结构化快照,包括同源 iframe 内容。", "en": "Get a structured page snapshot, including same-origin iframe content." }, - "parameters": [ - { "name": "filename", "description": { "zh": "可选,保存快照的文件名。", "en": "Optional snapshot output file name." }, "type": "string", "required": false }, - { "name": "selector", "description": { "zh": "可选,作为快照根节点的元素选择器。", "en": "Optional root element selector for a partial snapshot." }, "type": "string", "required": false }, - { "name": "depth", "description": { "zh": "可选,限制快照树深度。", "en": "Optional snapshot tree depth limit." }, "type": "number", "required": false } - ] - }, - { - "name": "type", - "description": { "zh": "向可编辑元素输入文本。", "en": "Type text into an editable element." }, - "parameters": [ - { "name": "ref", "description": { "zh": "目标元素 ref。", "en": "Target element ref." }, "type": "string", "required": true }, - { "name": "text", "description": { "zh": "输入文本。", "en": "Text to type." }, "type": "string", "required": true }, - { "name": "element", "description": { "zh": "可选,人类可读元素描述。", "en": "Optional human-readable element description." }, "type": "string", "required": false }, - { "name": "submit", "description": { "zh": "可选,输入后是否提交。", "en": "Optional submit after typing." }, "type": "boolean", "required": false }, - { "name": "slowly", "description": { "zh": "可选,是否逐字输入。", "en": "Optional type slowly." }, "type": "boolean", "required": false } - ] - }, - { - "name": "wait_for", - "description": { "zh": "等待文本出现、消失或等待指定时间。", "en": "Wait for text to appear, disappear, or for a duration." }, - "parameters": [ - { "name": "time", "description": { "zh": "可选,等待秒数。", "en": "Optional number of seconds to wait." }, "type": "number", "required": false }, - { "name": "text", "description": { "zh": "可选,等待出现的文本。", "en": "Optional text to wait for." }, "type": "string", "required": false }, - { "name": "textGone", "description": { "zh": "可选,等待消失的文本。", "en": "Optional text to wait to disappear." }, "type": "string", "required": false } - ] - }, - { - "name": "tabs", - "description": { "zh": "列出、创建、切换或关闭 tab。", "en": "List, create, select, or close tabs." }, - "parameters": [ - { "name": "action", "description": { "zh": "操作:list/create/select/close。", "en": "Action: list/create/select/close." }, "type": "string", "required": true }, - { "name": "index", "description": { "zh": "可选,0-based tab 索引。", "en": "Optional 0-based tab index." }, "type": "number", "required": false } - ] - }, - ] -}*/ - -const MAX_INLINE_BROWSER_TEXT_CHARS = 24000; -type BrowserTabAction = "list" | "create" | "select" | "close"; -type BrowserMouseButton = "left" | "right" | "middle"; -type BrowserModifierKey = "Alt" | "Control" | "ControlOrMeta" | "Meta" | "Shift"; -type ToolParamValue = string | number | boolean | object; - -interface FilenamePayload { - filename?: string; -} - -interface SnapshotPayload extends FilenamePayload { - selector?: string; - depth?: number; -} - -interface ClickPayload { - ref?: string; - selector?: string; - element?: string; - button?: BrowserMouseButton; - modifiers?: BrowserModifierKey[]; - doubleClick?: boolean; -} - -interface ConsoleMessagesPayload extends FilenamePayload { - level: string; -} - -interface EvaluatePayload { - function: string; - ref?: string; - element?: string; -} - -interface UploadPayload { - paths?: string[]; -} - -interface FillFormFieldPayload { - name: string; - type: string; - value: ToolParamValue; - ref?: string; - selector?: string; -} - -interface FillFormPayload { - fields: FillFormFieldPayload[]; -} - -interface HandleDialogPayload { - accept: boolean; - promptText?: string; -} - -interface HoverPayload { - ref: string; - element?: string; -} - -interface DragPayload { - startElement: string; - startRef: string; - endElement: string; - endRef: string; -} - -interface GotoPayload { - url: string; -} - -interface NetworkRequestsPayload extends FilenamePayload { - includeStatic?: boolean; -} - -interface PressKeyPayload { - key: string; -} - -interface ResizePayload { - width: number; - height: number; -} - -interface RunCodePayload { - code: string; -} - -interface SelectOptionPayload { - ref: string; - values: string[]; - element?: string; -} - -interface TypePayload { - ref: string; - text: string; - element?: string; - submit?: boolean; - slowly?: boolean; -} - -interface WaitForPayload { - time?: number; - text?: string; - textGone?: string; -} - -interface TabsPayload { - action: BrowserTabAction; - index?: number; -} - -const TOOL_NAMES = [ - "click", - "close", - "close_all", - "console_messages", - "drag", - "evaluate", - "upload", - "fill_form", - "handle_dialog", - "hover", - "goto", - "back", - "network_requests", - "press_key", - "resize", - "run_code", - "select_option", - "snapshot", - "type", - "wait_for", - "tabs" -]; - -function normalizeOptionalString(value?: string): string | undefined { - if (value === undefined) { - return undefined; - } - const normalized = value.trim(); - return normalized ? normalized : undefined; -} - -function buildLargeOutputFilename(prefix: string, extension: string): string { - const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); - const rand = Math.floor(Math.random() * 1000000); - return OPERIT_CLEAN_ON_EXIT_DIR + "/browser_" + prefix + "_" + timestamp + "_" + rand + "." + extension; -} - -async function maybePersistLargeBrowserResponse( - result: unknown, - prefix: string, - extension: string = "md" -) { - if (typeof result !== "string" || result.length <= MAX_INLINE_BROWSER_TEXT_CHARS) { - return result; - } - await Tools.Files.mkdir(OPERIT_CLEAN_ON_EXIT_DIR, true); - const filename = buildLargeOutputFilename(prefix, extension); - await Tools.Files.write(filename, result, false); - const normalizedPath = filename.replace(/\\/g, "/"); - return "Large browser response saved to:\n- [Browser Output](" + normalizedPath + ")"; -} - -async function click(params: ClickPayload) { - const result = await Tools.Net.browserClick(params); - return maybePersistLargeBrowserResponse(result, "click"); -} - -async function close() { - const result = await Tools.Net.browserClose(); - return maybePersistLargeBrowserResponse(result, "close"); -} - -async function close_all() { - const result = await Tools.Net.browserCloseAll(); - return maybePersistLargeBrowserResponse(result, "close_all"); -} - -async function console_messages(params: Partial = {}) { - const result = await Tools.Net.browserConsoleMessages(params); - return maybePersistLargeBrowserResponse(result, "console_messages"); -} - -async function drag(params: DragPayload) { - const result = await Tools.Net.browserDrag(params); - return maybePersistLargeBrowserResponse(result, "drag"); -} - -async function evaluate(params: EvaluatePayload) { - const result = await Tools.Net.browserEvaluate(params); - return maybePersistLargeBrowserResponse(result, "evaluate"); -} - -async function upload(params: UploadPayload = {}) { - const result = await Tools.Net.browserFileUpload(params); - return maybePersistLargeBrowserResponse(result, "upload"); -} - -function normalizeFormFields(fields: FillFormFieldPayload[]): FillFormFieldPayload[] { - if (fields.length === 0) { - throw new Error("fields must be a non-empty array"); - } - return fields.map((field, index) => { - const normalized: FillFormFieldPayload = { - name: field.name.trim(), - type: field.type.trim(), - value: field.value as ToolParamValue - }; - if (!normalized.name) { - throw new Error("fields[" + index + "].name is required"); - } - if (!normalized.type) { - throw new Error("fields[" + index + "].type is required"); - } - const ref = normalizeOptionalString(field.ref); - const selector = normalizeOptionalString(field.selector); - if (!ref && !selector) { - throw new Error("fields[" + index + "] requires ref or selector"); - } - if (ref) { - normalized.ref = ref; - } - if (selector) { - normalized.selector = selector; - } - return normalized; - }); -} - -async function fill_form(params: FillFormPayload) { - const result = await Tools.Net.browserFillForm({ - fields: normalizeFormFields(params.fields) - }); - return maybePersistLargeBrowserResponse(result, "fill_form"); -} - -async function handle_dialog(params: HandleDialogPayload) { - const result = await Tools.Net.browserHandleDialog(params); - return maybePersistLargeBrowserResponse(result, "handle_dialog"); -} - -async function hover(params: HoverPayload) { - const result = await Tools.Net.browserHover(params); - return maybePersistLargeBrowserResponse(result, "hover"); -} - -async function goto(params: GotoPayload) { - const result = await Tools.Net.browserNavigate(params); - return maybePersistLargeBrowserResponse(result, "goto"); -} - -async function back() { - const result = await Tools.Net.browserNavigateBack(); - return maybePersistLargeBrowserResponse(result, "back"); -} - -async function network_requests(params: NetworkRequestsPayload = {}) { - const result = await Tools.Net.browserNetworkRequests(params); - return maybePersistLargeBrowserResponse(result, "network_requests"); -} - -async function press_key(params: PressKeyPayload) { - const result = await Tools.Net.browserPressKey(params); - return maybePersistLargeBrowserResponse(result, "press_key"); -} - -async function resize(params: ResizePayload) { - const result = await Tools.Net.browserResize(params); - return maybePersistLargeBrowserResponse(result, "resize"); -} - -async function run_code(params: RunCodePayload) { - const result = await Tools.Net.browserRunCode(params); - return maybePersistLargeBrowserResponse(result, "run_code"); -} - -async function select_option(params: SelectOptionPayload) { - const result = await Tools.Net.browserSelectOption(params); - return maybePersistLargeBrowserResponse(result, "select_option"); -} - -async function snapshot(params: SnapshotPayload = {}) { - const result = await Tools.Net.browserSnapshot(params); - return maybePersistLargeBrowserResponse(result, "snapshot"); -} - -async function type(params: TypePayload) { - const result = await Tools.Net.browserType(params); - return maybePersistLargeBrowserResponse(result, "type"); -} - -async function wait_for(params: WaitForPayload = {}) { - const result = await Tools.Net.browserWaitFor(params); - return maybePersistLargeBrowserResponse(result, "wait_for"); -} - -async function tabs(params: TabsPayload) { - const result = await Tools.Net.browserTabs(params); - return maybePersistLargeBrowserResponse(result, "tabs"); -} - -async function browserMain() { - return "Browser package ready: " + TOOL_NAMES.join(", "); -} - -exports.click = click; -exports.close = close; -exports.close_all = close_all; -exports.console_messages = console_messages; -exports.drag = drag; -exports.evaluate = evaluate; -exports.upload = upload; -exports.fill_form = fill_form; -exports.handle_dialog = handle_dialog; -exports.hover = hover; -exports.goto = goto; -exports.back = back; -exports.network_requests = network_requests; -exports.press_key = press_key; -exports.resize = resize; -exports.run_code = run_code; -exports.select_option = select_option; -exports.snapshot = snapshot; -exports.type = type; -exports.wait_for = wait_for; -exports.tabs = tabs; -exports.main = browserMain; diff --git a/scripts/code_runner.ts b/scripts/code_runner.ts deleted file mode 100644 index f3b9be7..0000000 --- a/scripts/code_runner.ts +++ /dev/null @@ -1,1351 +0,0 @@ -/* METADATA -{ - name: code_runner - display_name: { - zh: "代码运行器" - en: "Code Runner" - } - description: { zh: "提供多语言代码执行能力,支持JavaScript、Python、Ruby、Go、Rust、C和C++脚本的运行。可直接执行代码字符串或运行外部文件,适用于快速测试、自动化脚本和教学演示。", en: "Multi-language code execution. Supports running JavaScript, Python, Ruby, Go, Rust, C and C++ scripts. You can execute code strings directly or run external files, useful for quick tests, automation, and demos." } - enabledByDefault: true - - category: "Development" - // Multiple tools in this package - tools: [ - { - name: run_javascript_es5 - description: { zh: "运行自定义 JavaScript (ES5) 脚本。会捕获 console.log 的输出以及最终的返回值。", en: "Run custom JavaScript (ES5). Captures console.log output and the final return value." } - // This tool takes parameters - parameters: [ - { - name: script - description: { zh: "要执行的 JavaScript 脚本内容", en: "JavaScript script content to execute." } - type: string - required: true - } - ] - }, - { - name: run_javascript_file - description: { zh: "运行 JavaScript (ES5) 文件。会捕获 console.log 的输出以及最终的返回值。", en: "Run a JavaScript (ES5) file. Captures console.log output and the final return value." } - parameters: [ - { - name: file_path - description: { zh: "JavaScript 文件路径", en: "Path to the JavaScript file." } - type: string - required: true - } - ] - }, - { - name: run_javascript_node - description: { zh: "使用 Node.js 运行 JavaScript 脚本。返回 stdout/stderr 输出。", en: "Run JavaScript using Node.js. Returns stdout/stderr output." } - parameters: [ - { - name: script - description: { zh: "要执行的 JavaScript 脚本内容", en: "JavaScript script content to execute." } - type: string - required: true - }, - { - name: node_flags - description: { zh: "Node.js 解释器选项,默认为空。可自定义如 --trace-warnings、--no-warnings 等", en: "Node.js interpreter flags (default: empty). Examples: --trace-warnings, --no-warnings." } - type: string - required: false - } - ] - }, - { - name: run_javascript_node_file - description: { zh: "使用 Node.js 运行 JavaScript 文件。返回 stdout/stderr 输出。", en: "Run a JavaScript file using Node.js. Returns stdout/stderr output." } - parameters: [ - { - name: file_path - description: { zh: "JavaScript 文件路径", en: "Path to the JavaScript file." } - type: string - required: true - }, - { - name: node_flags - description: { zh: "Node.js 解释器选项,默认为空。可自定义如 --trace-warnings、--no-warnings 等", en: "Node.js interpreter flags (default: empty). Examples: --trace-warnings, --no-warnings." } - type: string - required: false - } - ] - }, - { - name: install_node_packages - description: { zh: "在持久 Node 工作目录($HOME/.code_runner/node)中安装 pnpm 包", en: "Install packages with pnpm in the persistent Node workspace ($HOME/.code_runner/node)." } - parameters: [ - { - name: packages - description: { zh: "要安装的包名(用 | 分隔),例如 axios|lodash|@types/node", en: "Package names to install, separated by | (e.g. axios|lodash|@types/node)." } - type: string - required: true - }, - { - name: save_dev - description: { zh: "是否作为开发依赖安装(pnpm add -D)", en: "Whether to install as dev dependency (pnpm add -D)." } - type: boolean - required: false - } - ] - }, - { - name: install_python_packages - description: { zh: "在持久虚拟环境(~/.code_runner/py)中安装 Python 包(使用 pip)", en: "Install Python packages in the persistent virtual environment (~/.code_runner/py) using pip." } - parameters: [ - { - name: packages - description: { zh: "要安装的包名(用 | 分隔),例如 numpy|pydantic==2.*", en: "Package names to install, separated by | (e.g. numpy|pydantic==2.*)." } - type: string - required: true - }, - { - name: upgrade - description: { zh: "是否升级已安装的包,等价于 pip -U", en: "Whether to upgrade already installed packages (equivalent to pip -U)." } - type: boolean - required: false - } - ] - }, - { - name: run_python - description: { zh: "运行自定义 Python 脚本。会捕获 print 函数的输出。", en: "Run custom Python scripts. Captures output from print()." } - parameters: [ - { - name: script - description: { zh: "要执行的 Python 脚本内容", en: "Python script content to execute." } - type: string - required: true - }, - { - name: python_flags - description: { zh: "Python 解释器选项,默认为空。可自定义如 -O(优化)、-u(无缓冲)等", en: "Python interpreter flags (default: empty). Examples: -O (optimize), -u (unbuffered)." } - type: string - required: false - }, - { - name: script_args - description: { zh: "传递给 Python 脚本的参数,使用 | 分隔,例如 arg1|arg with space|--name=alice", en: "Arguments passed to the Python script, separated by | (e.g. arg1|arg with space|--name=alice)." } - type: string - required: false - } - ] - }, - { - name: run_python_file - description: { zh: "运行 Python 文件。会捕获 print 函数的输出。", en: "Run a Python file. Captures output from print()." } - parameters: [ - { - name: file_path - description: { zh: "Python 文件路径", en: "Path to the Python file." } - type: string - required: true - }, - { - name: python_flags - description: { zh: "Python 解释器选项,默认为空。可自定义如 -O(优化)、-u(无缓冲)等", en: "Python interpreter flags (default: empty). Examples: -O (optimize), -u (unbuffered)." } - type: string - required: false - }, - { - name: script_args - description: { zh: "传递给 Python 文件的参数,使用 | 分隔,例如 arg1|arg with space|--name=alice", en: "Arguments passed to the Python file, separated by | (e.g. arg1|arg with space|--name=alice)." } - type: string - required: false - } - ] - }, - { - name: run_ruby - description: { zh: "运行自定义 Ruby 脚本", en: "Run custom Ruby scripts." } - parameters: [ - { - name: script - description: { zh: "要执行的 Ruby 脚本内容", en: "Ruby script content to execute." } - type: string - required: true - }, - { - name: ruby_flags - description: { zh: "Ruby 解释器选项,默认为空。可自定义如 --jit(JIT 编译)等", en: "Ruby interpreter flags (default: empty). Example: --jit." } - type: string - required: false - } - ] - }, - { - name: run_ruby_file - description: { zh: "运行 Ruby 文件", en: "Run a Ruby file." } - parameters: [ - { - name: file_path - description: { zh: "Ruby 文件路径", en: "Path to the Ruby file." } - type: string - required: true - }, - { - name: ruby_flags - description: { zh: "Ruby 解释器选项,默认为空。可自定义如 --jit(JIT 编译)等", en: "Ruby interpreter flags (default: empty). Example: --jit." } - type: string - required: false - } - ] - }, - { - name: run_go - description: { zh: "运行自定义 Go 代码", en: "Run custom Go code." } - parameters: [ - { - name: script - description: { zh: "要执行的 Go 代码内容", en: "Go source code to execute." } - type: string - required: true - }, - { - name: build_flags - description: { zh: "Go 编译选项,默认为空。可自定义如 -ldflags='-s -w'(减小二进制体积)等", en: "Go build flags (default: empty). Example: -ldflags='-s -w' (reduce binary size)." } - type: string - required: false - } - ] - }, - { - name: run_go_file - description: { zh: "运行 Go 文件", en: "Run a Go file." } - parameters: [ - { - name: file_path - description: { zh: "Go 文件路径", en: "Path to the Go file." } - type: string - required: true - }, - { - name: build_flags - description: { zh: "Go 编译选项,默认为空。可自定义如 -ldflags='-s -w'(减小二进制体积)等", en: "Go build flags (default: empty). Example: -ldflags='-s -w' (reduce binary size)." } - type: string - required: false - } - ] - }, - { - name: run_rust - description: { zh: "运行自定义 Rust 代码", en: "Run custom Rust code." } - parameters: [ - { - name: script - description: { zh: "要执行的 Rust 代码内容", en: "Rust source code to execute." } - type: string - required: true - }, - { - name: cargo_flags - description: { zh: "Cargo 构建选项,默认为 --release。可自定义如 空字符串(调试模式)、--release --features xxx 等", en: "Cargo build flags (default: --release). Examples: empty string (debug), --release --features xxx." } - type: string - required: false - } - ] - }, - { - name: run_rust_file - description: { zh: "运行 Rust 文件", en: "Run a Rust file." } - parameters: [ - { - name: file_path - description: { zh: "Rust 文件路径", en: "Path to the Rust file." } - type: string - required: true - }, - { - name: cargo_flags - description: { zh: "Cargo 构建选项,默认为 --release。可自定义如 空字符串(调试模式)、--release --features xxx 等", en: "Cargo build flags (default: --release). Examples: empty string (debug), --release --features xxx." } - type: string - required: false - } - ] - }, - { - name: run_c - description: { zh: "运行自定义 C 代码", en: "Run custom C code." } - parameters: [ - { - name: script - description: { zh: "要执行的 C 代码内容", en: "C source code to execute." } - type: string - required: true - }, - { - name: compile_flags - description: { zh: "编译选项,默认为 -O3 -march=native -fopenmp。可自定义如 -O2、-O0 -g 等", en: "Compile flags (default: -O3 -march=native -fopenmp). Examples: -O2, -O0 -g." } - type: string - required: false - } - ] - }, - { - name: run_c_file - description: { zh: "运行 C 文件", en: "Run a C file." } - parameters: [ - { - name: file_path - description: { zh: "C 文件路径", en: "Path to the C file." } - type: string - required: true - }, - { - name: compile_flags - description: { zh: "编译选项,默认为 -O3 -march=native -fopenmp。可自定义如 -O2、-O0 -g 等", en: "Compile flags (default: -O3 -march=native -fopenmp). Examples: -O2, -O0 -g." } - type: string - required: false - } - ] - }, - { - name: run_cpp - description: { zh: "运行自定义 C++ 代码", en: "Run custom C++ code." } - parameters: [ - { - name: script - description: { zh: "要执行的 C++ 代码内容", en: "C++ source code to execute." } - type: string - required: true - }, - { - name: compile_flags - description: { zh: "编译选项,默认为 -O3 -march=native -fopenmp。可自定义如 -O2、-O0 -g 等", en: "Compile flags (default: -O3 -march=native -fopenmp). Examples: -O2, -O0 -g." } - type: string - required: false - } - ] - }, - { - name: run_cpp_file - description: { zh: "运行 C++ 文件", en: "Run a C++ file." } - parameters: [ - { - name: file_path - description: { zh: "C++ 文件路径", en: "Path to the C++ file." } - type: string - required: true - }, - { - name: compile_flags - description: { zh: "编译选项,默认为 -O3 -march=native -fopenmp。可自定义如 -O2、-O0 -g 等", en: "Compile flags (default: -O3 -march=native -fopenmp). Examples: -O2, -O0 -g." } - type: string - required: false - } - ] - } - ] -}*/ - -const codeRunner = (function () { - - const CARGO_MIRROR_ENV = 'export CARGO_REGISTRIES_CRATES_IO_REPLACE_WITH="ustc" && export CARGO_REGISTRIES_USTC_INDEX="https://mirrors.ustc.edu.cn/crates.io-index"'; - const CODE_RUNNER_HIDDEN_EXECUTOR_KEY = "code_runner_hidden_executor"; - const NODE_WORKSPACE_DIR = "$HOME/.code_runner/node"; - - // Use a persistent hidden executor so commands share one login context without shell wrapping. - async function executeTerminalCommand(command: string, timeoutMs?: number): Promise { - const options: { executorKey: string; timeoutMs?: number } = { - executorKey: CODE_RUNNER_HIDDEN_EXECUTOR_KEY - }; - if (timeoutMs !== undefined) { - options.timeoutMs = timeoutMs; - } - return await Tools.System.terminal.hiddenExec(command, options); - } - - // Ensure a persistent Python venv under ~/.code_runner/py and return python/pip paths - async function ensurePersistentVenv(): Promise<{ pythonBin: string; pipBin: string }> { - const venvDir = "~/.code_runner/py"; - const pythonBin = `${venvDir}/bin/python`; - const pipBin = `${venvDir}/bin/pip`; - - const exists = await executeTerminalCommand(`[ -x ${pythonBin} ] && echo OK || echo NO`); - if (exists.output.includes("OK")) { - return { pythonBin, pipBin }; - } - - const setup = await executeTerminalCommand(`python3 -m venv ${venvDir}`); - if (setup.exitCode !== 0 || hasError(setup.output)) { - throw new Error(`创建持久 venv 失败:\n${setup.output}`); - } - return { pythonBin, pipBin }; - } - - // Install Python packages into the persistent venv using pip - async function install_python_packages(params: { packages: string; upgrade?: boolean }) { - const raw = (params.packages || "").trim(); - const pkgs = raw - .split("|") - .map(s => s.trim()) - .filter(s => s.length > 0); - if (!pkgs.length) throw new Error("请提供要安装的包列表(用 | 分隔)packages"); - - const upgradeFlag = params.upgrade ? "-U" : ""; - const { pythonBin } = await ensurePersistentVenv(); - - // Fallback: use python -m pip to avoid missing pip executable shims - const r2 = await executeTerminalCommand(`${pythonBin} -m pip install ${upgradeFlag} ${pkgs.join(" ")}`.trim()); - if (r2.exitCode !== 0 || hasError(r2.output)) { - throw new Error(`安装依赖失败:\n${r2.output}`); - } - return `Installed with pip:\n${r2.output}`.trim(); - } - - async function ensureNodeAvailable() { - const nodeCheckResult = await executeTerminalCommand("node --version"); - if (nodeCheckResult.exitCode !== 0 || hasError(nodeCheckResult.output)) { - throw new Error("Node.js 不可用,请确保已安装 Node.js"); - } - } - - async function ensurePersistentNodeWorkspace(): Promise<{ workspaceDir: string }> { - await ensureNodeAvailable(); - - const createDirResult = await executeTerminalCommand(`mkdir -p ${NODE_WORKSPACE_DIR}`); - if (createDirResult.exitCode !== 0 || hasError(createDirResult.output)) { - throw new Error(`创建 Node 工作目录失败:\n${createDirResult.output}`); - } - - const hasPackageJson = await executeTerminalCommand(`[ -f ${NODE_WORKSPACE_DIR}/package.json ] && echo OK || echo NO`); - if (!hasPackageJson.output.includes("OK")) { - const initResult = await executeTerminalCommand( - `cat <<'EOF' > ${NODE_WORKSPACE_DIR}/package.json -{ - "name": "code-runner-node-workspace", - "private": true -} -EOF` - ); - if (initResult.exitCode !== 0 || hasError(initResult.output)) { - throw new Error(`初始化 Node 工作目录失败:\n${initResult.output}`); - } - } - - return { workspaceDir: NODE_WORKSPACE_DIR }; - } - - async function install_node_packages(params: { packages: string; save_dev?: boolean }) { - const raw = (params.packages || "").trim(); - const pkgs = raw - .split("|") - .map(s => s.trim()) - .filter(s => s.length > 0); - if (!pkgs.length) throw new Error("请提供要安装的包列表(用 | 分隔)packages"); - - const { workspaceDir } = await ensurePersistentNodeWorkspace(); - const saveFlag = params.save_dev ? "-D" : "--save"; - const packageArgs = pkgs.map(p => `'${escapeForShell(p)}'`).join(" "); - const result = await executeTerminalCommand(`cd ${workspaceDir} && pnpm add ${saveFlag} ${packageArgs}`); - if (result.exitCode !== 0 || hasError(result.output)) { - throw new Error(`安装 pnpm 依赖失败:\n${result.output}`); - } - return `Installed with pnpm in ${workspaceDir}:\n${result.output}`.trim(); - } - - // Helper function to safely escape strings for shell commands - function escapeForShell(str: string): string { - return str.replace(/'/g, "'\\''"); - } - - function buildPipeSeparatedShellArgs(raw?: string): string { - if (!raw || raw.trim() === "") { - return ""; - } - - return raw - .split("|") - .map(part => part.trim()) - .filter(part => part.length > 0) - .map(part => `'${escapeForShell(part)}'`) - .join(" "); - } - - // Helper function to execute JavaScript code and capture logs/return value - async function executeJavaScript(script: string): Promise { - const logs: string[] = []; - - // Create a proxy for the console object - const consoleProxy = { - log: (...args: any[]) => { - const formattedArgs = args.map(arg => { - if (arg === null) return 'null'; - if (arg === undefined) return 'undefined'; - if (typeof arg === 'object') { - try { return JSON.stringify(arg, null, 2); } catch (e) { return arg.toString(); } - } - return arg.toString(); - }); - logs.push(formattedArgs.join(' ')); - }, - warn: (...args: any[]) => { - const formattedArgs = args.map(arg => { - if (arg === null) return 'null'; - if (arg === undefined) return 'undefined'; - if (typeof arg === 'object') { - try { return JSON.stringify(arg, null, 2); } catch (e) { return arg.toString(); } - } - return arg.toString(); - }); - logs.push(`WARN: ${formattedArgs.join(' ')}`); - }, - error: (...args: any[]) => { - const formattedArgs = args.map(arg => { - if (arg === null) return 'null'; - if (arg === undefined) return 'undefined'; - if (typeof arg === 'object') { - try { return JSON.stringify(arg, null, 2); } catch (e) { return arg.toString(); } - } - return arg.toString(); - }); - logs.push(`ERROR: ${formattedArgs.join(' ')}`); - }, - }; - - try { - // Use the Function constructor to create a new function in the global scope. - // Pass the console proxy to it. This is safer than eval. - const func = new Function('console', ` - // Wrap in an IIFE to handle return statements properly and avoid variable leakage. - return (function(){ - "use strict"; - ${script} - })(); - `); - - const returnValue = func(consoleProxy); - - let output = logs.join('\n'); - - if (returnValue !== undefined) { - if (output) { - output += '\n'; - } - let formattedReturnValue; - if (returnValue === null) { - formattedReturnValue = 'null'; - } else if (typeof returnValue === 'object') { - try { - formattedReturnValue = JSON.stringify(returnValue, null, 2); - } catch (e) { - formattedReturnValue = returnValue.toString(); - } - } else { - formattedReturnValue = returnValue.toString(); - } - output += `Return value: ${formattedReturnValue}`; - } - - return output || "(No output from console.log or return value)"; - - } catch (e: any) { - const errorOutput = logs.join('\n'); - if (errorOutput) { - throw new Error(`Script execution failed: ${e.message}\n\nLogs before error:\n${errorOutput}`); - } else { - throw new Error(`Script execution failed: ${e.message}`); - } - } - } - - // Helper function to check for errors in command output when exit code is unreliable - function hasError(output: string): boolean { - const errorPatterns = [ - "command not found", - "No such file or directory", - "error:", - "Error:", - "failed", - "Failed", - "unable", - "Unable" - ]; - const lowercasedOutput = output.toLowerCase(); - return errorPatterns.some(pattern => lowercasedOutput.includes(pattern)); - } - - async function main() { - // Ensure /tmp exists - await executeTerminalCommand("mkdir -p /tmp"); - - const results = { - javascript: await testJavaScript(), - python: await testPython(), - ruby: await testRuby(), - go: await testGo(), - rust: await testRust(), - c: await testC(), - cpp: await testCpp() - }; - - // Format results for display - let summary = "代码执行器功能测试结果:\n"; - for (const [lang, result] of Object.entries(results)) { - summary += `${lang}: ${result.success ? '✅ 成功' : '❌ 失败'} - ${result.message}\n`; - } - - return summary; - } - - // 测试JavaScript执行功能 - async function testJavaScript() { - try { - // 测试简单的JS代码 - const script = "console.log('JavaScript 运行正常'); const testVar = 42; return '测试值: ' + testVar;"; - const result = await executeJavaScript(script); - const expectedOutput = `JavaScript 运行正常\nReturn value: "测试值: 42"`; - - if (result !== expectedOutput) { - return { success: false, message: `JavaScript执行器测试失败: 期望 "${expectedOutput}", 实际 "${result}"` }; - } - return { success: true, message: "JavaScript执行器测试成功" }; - } catch (error) { - return { success: false, message: `JavaScript执行器测试失败: ${error.message}` }; - } - } - - // 测试Python执行功能 - async function testPython() { - try { - // 检查Python是否可用 - const pythonCheckResult = await executeTerminalCommand("python3 --version"); - if (pythonCheckResult.exitCode !== 0 || hasError(pythonCheckResult.output)) { - return { success: false, message: "Python不可用,请确保已安装Python" }; - } - - // 测试简单的Python代码 - const script = "print('Python运行正常')"; - const tempPyFile = "/tmp/test_python.py"; - await executeTerminalCommand(`cat <<'EOF' > ${tempPyFile}\n${script}\nEOF`); - const runResult = await executeTerminalCommand(`python3 ${tempPyFile}`); - await executeTerminalCommand(`rm -f ${tempPyFile}`); - - if (runResult.exitCode !== 0 || hasError(runResult.output) || !runResult.output.includes("Python运行正常")) { - return { success: false, message: `Python执行器测试失败: ${runResult.output}` }; - } - return { success: true, message: "Python执行器测试成功" }; - } catch (error) { - return { success: false, message: `Python执行器测试失败: ${error.message}` }; - } - } - - // 测试Ruby执行功能 - async function testRuby() { - try { - // 检查Ruby是否可用 - const rubyCheckResult = await executeTerminalCommand("ruby --version"); - if (rubyCheckResult.exitCode !== 0 || hasError(rubyCheckResult.output)) { - return { success: false, message: "Ruby不可用,请确保已安装Ruby" }; - } - - // 测试简单的Ruby代码 - const script = "puts 'Ruby运行正常'"; - const tempRbFile = "/tmp/test_ruby.rb"; - await executeTerminalCommand(`cat <<'EOF' > ${tempRbFile}\n${script}\nEOF`); - const runResult = await executeTerminalCommand(`ruby ${tempRbFile}`); - await executeTerminalCommand(`rm -f ${tempRbFile}`); - - if (runResult.exitCode !== 0 || hasError(runResult.output) || !runResult.output.includes("Ruby运行正常")) { - return { success: false, message: `Ruby执行器测试失败: ${runResult.output}` }; - } - return { success: true, message: "Ruby执行器测试成功" }; - } catch (error) { - return { success: false, message: `Ruby执行器测试失败: ${error.message}` }; - } - } - - // 测试Go执行功能 - async function testGo() { - try { - // 检查Go是否可用 - const goCheckResult = await executeTerminalCommand("go version"); - if (goCheckResult.exitCode !== 0 || hasError(goCheckResult.output)) { - return { success: false, message: "Go不可用,请确保已安装Go" }; - } - - // 测试简单的Go代码 - const script = ` -package main -import "fmt" -func main() { - fmt.Println("Go运行正常") -}`; - const tempGoDir = "/tmp/test_go_project"; - const tempGoFile = `${tempGoDir}/main.go`; - const tempGoExec = `${tempGoDir}/main`; - await executeTerminalCommand(`mkdir -p ${tempGoDir}`); - await executeTerminalCommand(`cat <<'EOF' > ${tempGoFile}\n${script}\nEOF`); - - const compileResult = await executeTerminalCommand(`cd ${tempGoDir} && go build -o main main.go`); - if (compileResult.exitCode !== 0 || hasError(compileResult.output)) { - await executeTerminalCommand(`rm -rf ${tempGoDir}`); - return { success: false, message: `Go 编译失败: ${compileResult.output}` }; - } - - const runResult = await executeTerminalCommand(tempGoExec); - await executeTerminalCommand(`rm -rf ${tempGoDir}`); - - if (runResult.exitCode !== 0 || hasError(runResult.output) || !runResult.output.includes("Go运行正常")) { - return { success: false, message: `Go 执行失败: ${runResult.output}` }; - } - - return { success: true, message: "Go执行器测试成功" }; - } catch (error) { - return { success: false, message: `Go执行器测试失败: ${error.message}` }; - } - } - - // 检查并配置Rust环境 - async function ensureRustConfigured(): Promise<{ success: boolean; message: string }> { - // 在有效目录中运行,避免 "Could not locate working directory" 错误 - let rustCheckResult = await executeTerminalCommand("cd /tmp && rustc --version"); - - if (rustCheckResult.exitCode === 0 && !hasError(rustCheckResult.output)) { - return { success: true, message: "Rust环境已配置" }; - } - - // 如果未配置默认工具链,则尝试设置 - if (rustCheckResult.output.includes("no default is configured")) { - const setupResult = await executeTerminalCommand('export RUSTUP_DIST_SERVER="https://mirrors.ustc.edu.cn/rust-static" && export RUSTUP_UPDATE_ROOT="https://mirrors.ustc.edu.cn/rust-static/rustup" && rustup default stable'); - if (setupResult.exitCode !== 0 || hasError(setupResult.output)) { - return { success: false, message: `运行 'rustup default stable' 失败: ${setupResult.output}` }; - } - - // 再次检查 - rustCheckResult = await executeTerminalCommand("cd /tmp && rustc --version"); - if (rustCheckResult.exitCode === 0 && !hasError(rustCheckResult.output)) { - return { success: true, message: "Rust环境已自动配置" }; - } - } - - return { success: false, message: `Rust环境检查失败: ${rustCheckResult.output}` }; - } - - // 测试Rust执行功能 - async function testRust() { - try { - const rustConfig = await ensureRustConfigured(); - if (!rustConfig.success) { - return { success: false, message: rustConfig.message }; - } - - // 测试简单的Rust代码 - const script = ` -fn main() { - println!("Rust运行正常"); -}`; - const tempRustDir = "/tmp/test_rust_project"; - const tempRustSrcDir = `${tempRustDir}/src`; - const tempRustFile = `${tempRustSrcDir}/main.rs`; - const cargoToml = ` -[package] -name = "test_rust" -version = "0.1.0" -edition = "2021" -[dependencies] -`; - await executeTerminalCommand(`mkdir -p ${tempRustSrcDir}`); - await executeTerminalCommand(`cat <<'EOF' > ${tempRustDir}/Cargo.toml\n${cargoToml}\nEOF`); - await executeTerminalCommand(`cat <<'EOF' > ${tempRustFile}\n${script}\nEOF`); - - // 在有效目录中运行 cargo - const compileResult = await executeTerminalCommand(`cd ${tempRustDir} && ${CARGO_MIRROR_ENV} && cargo build --release`); - if (compileResult.exitCode !== 0 || hasError(compileResult.output)) { - await executeTerminalCommand(`rm -rf ${tempRustDir}`); - return { success: false, message: `Rust 编译失败: ${compileResult.output}` }; - } - - const execPath = `${tempRustDir}/target/release/test_rust`; - const runResult = await executeTerminalCommand(execPath); - await executeTerminalCommand(`rm -rf ${tempRustDir}`); - - if (runResult.exitCode !== 0 || hasError(runResult.output) || !runResult.output.includes("Rust运行正常")) { - return { success: false, message: `Rust 执行失败: ${runResult.output}` }; - } - - return { success: true, message: "Rust执行器测试成功" }; - } catch (error) { - return { success: false, message: `Rust执行器测试失败: ${error.message}` }; - } - } - - // 测试C执行功能 - async function testC() { - try { - // 检查gcc是否可用 - const gccCheckResult = await executeTerminalCommand("gcc --version"); - if (gccCheckResult.exitCode !== 0 || hasError(gccCheckResult.output)) { - return { success: false, message: "GCC不可用,请确保已安装gcc" }; - } - - // 测试简单的C代码 - const script = ` -#include -int main() { - printf("C运行正常\\n"); - return 0; -}`; - const tempCFile = "/tmp/test_c.c"; - const tempCExec = "/tmp/test_c"; - await executeTerminalCommand(`cat <<'EOF' > ${tempCFile}\n${script}\nEOF`); - - const compileResult = await executeTerminalCommand(`gcc -O3 -march=native -fopenmp ${tempCFile} -o ${tempCExec}`); - if (compileResult.exitCode !== 0 || hasError(compileResult.output)) { - await executeTerminalCommand(`rm -f ${tempCFile} ${tempCExec}`); - return { success: false, message: `C 编译失败: ${compileResult.output}` }; - } - - const runResult = await executeTerminalCommand(tempCExec); - await executeTerminalCommand(`rm -f ${tempCFile} ${tempCExec}`); - - if (runResult.exitCode !== 0 || hasError(runResult.output) || !runResult.output.includes("C运行正常")) { - return { success: false, message: `C 执行失败: ${runResult.output}` }; - } - - return { success: true, message: "C执行器测试成功" }; - } catch (error) { - return { success: false, message: `C执行器测试失败: ${error.message}` }; - } - } - - // 测试C++执行功能 - async function testCpp() { - try { - // 检查g++是否可用 - const gppCheckResult = await executeTerminalCommand("g++ --version"); - if (gppCheckResult.exitCode !== 0 || hasError(gppCheckResult.output)) { - return { success: false, message: "G++不可用,请确保已安装g++" }; - } - - // 测试简单的C++代码 - const script = ` -#include -int main() { - std::cout << "C++运行正常" << std::endl; - return 0; -}`; - const tempCppFile = "/tmp/test_cpp.cpp"; - const tempCppExec = "/tmp/test_cpp"; - await executeTerminalCommand(`cat <<'EOF' > ${tempCppFile}\n${script}\nEOF`); - - const compileResult = await executeTerminalCommand(`g++ -O3 -march=native -fopenmp ${tempCppFile} -o ${tempCppExec}`); - if (compileResult.exitCode !== 0 || hasError(compileResult.output)) { - await executeTerminalCommand(`rm -f ${tempCppFile} ${tempCppExec}`); - return { success: false, message: `C++ 编译失败: ${compileResult.output}` }; - } - - const runResult = await executeTerminalCommand(tempCppExec); - await executeTerminalCommand(`rm -f ${tempCppFile} ${tempCppExec}`); - - if (runResult.exitCode !== 0 || hasError(runResult.output) || !runResult.output.includes("C++运行正常")) { - return { success: false, message: `C++ 执行失败: ${runResult.output}` }; - } - - return { success: true, message: "C++执行器测试成功" }; - } catch (error) { - return { success: false, message: `C++执行器测试失败: ${error.message}` }; - } - } - - async function run_javascript_es5(params: { script: string }) { - const script = params.script; - if (!script || script.trim() === "") { - throw new Error("请提供要执行的脚本内容"); - } - return executeJavaScript(script); - } - - async function run_javascript_file(params: { file_path: string }) { - const filePath = params.file_path; - - if (!filePath || filePath.trim() === "") { - throw new Error("请提供要执行的 JavaScript 文件路径"); - } - - const fileResult = await Tools.Files.read(filePath); - - if (!fileResult || !fileResult.content) { - throw new Error(`无法读取文件: ${filePath}`); - } - - return executeJavaScript(fileResult.content); - } - - async function run_javascript_node(params: { script: string; node_flags?: string }) { - const script = params.script; - if (!script || script.trim() === "") { - throw new Error("请提供要执行的 JavaScript 脚本内容"); - } - - const { workspaceDir } = await ensurePersistentNodeWorkspace(); - const nodeFlags = params.node_flags || ""; - const tempFileName = `temp_script_node_${Date.now()}.js`; - const tempFilePath = `${workspaceDir}/${tempFileName}`; - try { - await executeTerminalCommand(`cat <<'EOF' > ${tempFilePath}\n${script}\nEOF`); - const result = await executeTerminalCommand(`cd ${workspaceDir} && NODE_PATH=${workspaceDir}/node_modules node ${nodeFlags} ${tempFileName}`.trim()); - if (result.exitCode === 0 && !hasError(result.output)) { - return result.output.trim(); - } else { - throw new Error(`JavaScript (Node.js) 脚本执行失败:\n${result.output}`); - } - } finally { - await executeTerminalCommand(`rm -f ${tempFilePath}`).catch(err => console.error(`删除临时文件失败: ${err.message}`)); - } - } - - async function run_javascript_node_file(params: { file_path: string; node_flags?: string }) { - const filePath = params.file_path; - if (!filePath || filePath.trim() === "") { - throw new Error("请提供要执行的 JavaScript 文件路径"); - } - - const { workspaceDir } = await ensurePersistentNodeWorkspace(); - const escapedPath = escapeForShell(filePath); - const fileExistsResult = await executeTerminalCommand(`test -f '${escapedPath}'`); - if (fileExistsResult.exitCode !== 0 || hasError(fileExistsResult.output)) { - throw new Error(`JavaScript 文件不存在或路径错误: ${filePath}`); - } - - const nodeFlags = params.node_flags || ""; - const result = await executeTerminalCommand(`cd ${workspaceDir} && NODE_PATH=${workspaceDir}/node_modules node ${nodeFlags} '${escapedPath}'`.trim()); - if (result.exitCode === 0 && !hasError(result.output)) { - return result.output.trim(); - } else { - throw new Error(`JavaScript (Node.js) 文件执行失败:\n${result.output}`); - } - } - - async function run_python(params: { script: string; python_flags?: string; script_args?: string }) { - const script = params.script; - - if (!script || script.trim() === "") { - throw new Error("请提供要执行的 Python 脚本内容"); - } - - // Use persistent venv interpreter - const { pythonBin } = await ensurePersistentVenv(); - const pythonFlags = params.python_flags || ""; - const scriptArgs = buildPipeSeparatedShellArgs(params.script_args); - const tempFilePath = "/tmp/temp_script.py"; - const escapedTempFilePath = escapeForShell(tempFilePath); - try { - await executeTerminalCommand(`cat <<'EOF' > ${tempFilePath}\n${script}\nEOF`); - const result = await executeTerminalCommand(`${pythonBin} ${pythonFlags} '${escapedTempFilePath}' ${scriptArgs}`.trim()); - if (result.exitCode === 0 && !hasError(result.output)) { - return result.output.trim(); - } else { - throw new Error(`Python 脚本执行失败:\n${result.output}`); - } - } finally { - await executeTerminalCommand(`rm -f ${tempFilePath}`).catch(err => console.error(`删除临时文件失败: ${err.message}`)); - } - } - - async function run_python_file(params: { file_path: string; python_flags?: string; script_args?: string }) { - const filePath = params.file_path; - - if (!filePath || filePath.trim() === "") { - throw new Error("请提供要执行的 Python 文件路径"); - } - - const escapedPath = escapeForShell(filePath); - const fileExistsResult = await executeTerminalCommand(`test -f '${escapedPath}'`); - if (fileExistsResult.exitCode !== 0 || hasError(fileExistsResult.output)) { - throw new Error(`Python 文件不存在或路径错误: ${filePath}`); - } - - // Use persistent venv interpreter - const { pythonBin } = await ensurePersistentVenv(); - const pythonFlags = params.python_flags || ""; - const scriptArgs = buildPipeSeparatedShellArgs(params.script_args); - const result = await executeTerminalCommand(`${pythonBin} ${pythonFlags} '${escapedPath}' ${scriptArgs}`.trim()); - if (result.exitCode === 0 && !hasError(result.output)) { - return result.output.trim(); - } else { - throw new Error(`Python 文件执行失败:\n${result.output}`); - } - } - - async function run_ruby(params: { script: string; ruby_flags?: string }) { - const script = params.script; - if (!script || script.trim() === "") { - throw new Error("请提供要执行的 Ruby 脚本内容"); - } - - const rubyFlags = params.ruby_flags || ""; - const tempFilePath = "/tmp/temp_script.rb"; - try { - await executeTerminalCommand(`cat <<'EOF' > ${tempFilePath}\n${script}\nEOF`); - const result = await executeTerminalCommand(`ruby ${rubyFlags} ${tempFilePath}`); - if (result.exitCode === 0 && !hasError(result.output)) { - return result.output.trim(); - } else { - throw new Error(`Ruby 脚本执行失败:\n${result.output}`); - } - } finally { - await executeTerminalCommand(`rm -f ${tempFilePath}`).catch(err => console.error(`删除临时文件失败: ${err.message}`)); - } - } - - async function run_ruby_file(params: { file_path: string; ruby_flags?: string }) { - const filePath = params.file_path; - if (!filePath || filePath.trim() === "") { - throw new Error("请提供要执行的 Ruby 文件路径"); - } - - const fileExistsResult = await executeTerminalCommand(`test -f ${filePath}`); - if (fileExistsResult.exitCode !== 0 || hasError(fileExistsResult.output)) { - throw new Error(`Ruby 文件不存在或路径错误: ${filePath}`); - } - - const rubyFlags = params.ruby_flags || ""; - const result = await executeTerminalCommand(`ruby ${rubyFlags} ${filePath}`); - if (result.exitCode === 0 && !hasError(result.output)) { - return result.output.trim(); - } else { - throw new Error(`Ruby 文件执行失败:\n${result.output}`); - } - } - - async function run_go(params: { script: string; build_flags?: string }) { - const script = params.script; - - if (!script || script.trim() === "") { - throw new Error("请提供要执行的 Go 代码内容"); - } - - const buildFlags = params.build_flags || ""; - const tempDirPath = "/tmp/temp_go"; - const tempFilePath = `${tempDirPath}/main.go`; - - try { - await executeTerminalCommand(`mkdir -p ${tempDirPath}`); - await executeTerminalCommand(`cat <<'EOF' > ${tempFilePath}\n${script}\nEOF`); - - const compileResult = await executeTerminalCommand(`cd ${tempDirPath} && go build ${buildFlags} -o main main.go`); - if (compileResult.exitCode !== 0 || hasError(compileResult.output)) { - throw new Error(`Go 代码编译失败:\n${compileResult.output}`); - } - - const result = await executeTerminalCommand(`${tempDirPath}/main`); - - if (result.exitCode === 0 && !hasError(result.output)) { - return result.output.trim(); - } else { - throw new Error(`Go 代码执行失败:\n${result.output}`); - } - } finally { - await executeTerminalCommand(`rm -rf ${tempDirPath}`).catch(err => console.error(`删除临时目录失败: ${err.message}`)); - } - } - - async function run_go_file(params: { file_path: string; build_flags?: string }) { - const filePath = params.file_path; - if (!filePath || filePath.trim() === "") { - throw new Error("请提供要执行的 Go 文件路径"); - } - - const fileExistsResult = await executeTerminalCommand(`test -f ${filePath}`); - if (fileExistsResult.exitCode !== 0 || hasError(fileExistsResult.output)) { - throw new Error(`Go 文件不存在或路径错误: ${filePath}`); - } - - const buildFlags = params.build_flags || ""; - const tempExecPath = "/tmp/temp_go_exec"; - try { - const compileResult = await executeTerminalCommand(`go build ${buildFlags} -o ${tempExecPath} ${filePath}`); - if (compileResult.exitCode !== 0 || hasError(compileResult.output)) { - throw new Error(`Go 文件编译失败:\n${compileResult.output}`); - } - - const result = await executeTerminalCommand(tempExecPath); - - if (result.exitCode === 0 && !hasError(result.output)) { - return result.output.trim(); - } else { - throw new Error(`Go 文件执行失败:\n${result.output}`); - } - } finally { - await executeTerminalCommand(`rm -f ${tempExecPath}`).catch(err => console.error(`删除临时文件失败: ${err.message}`)); - } - } - - async function run_rust(params: { script: string; cargo_flags?: string }) { - const script = params.script; - if (!script || script.trim() === "") { - throw new Error("请提供要执行的 Rust 代码内容"); - } - - const rustConfig = await ensureRustConfigured(); - if (!rustConfig.success) { - throw new Error(rustConfig.message); - } - - const cargoFlags = params.cargo_flags || "--release"; - const buildMode = cargoFlags.includes("--release") ? "release" : "debug"; - const tempDirPath = "/tmp/temp_rust_project"; - try { - const cargoToml = ` -[package] -name = "temp_rust_script" -version = "0.1.0" -edition = "2021" - -[dependencies] - `; - await executeTerminalCommand(`mkdir -p ${tempDirPath}/src`, 10000); - await executeTerminalCommand(`cat <<'EOF' > ${tempDirPath}/Cargo.toml\n${cargoToml}\nEOF`); - await executeTerminalCommand(`cat <<'EOF' > ${tempDirPath}/src/main.rs\n${script}\nEOF`); - - const compileResult = await executeTerminalCommand(`cd ${tempDirPath} && ${CARGO_MIRROR_ENV} && cargo build ${cargoFlags}`); - if (compileResult.exitCode !== 0 || hasError(compileResult.output)) { - throw new Error(`Rust 代码编译失败:\n${compileResult.output}`); - } - - const execPath = `${tempDirPath}/target/${buildMode}/temp_rust_script`; - const result = await executeTerminalCommand(execPath, 30000); - if (result.exitCode === 0 && !hasError(result.output)) { - return result.output.trim(); - } else { - throw new Error(`Rust 代码执行失败:\n${result.output}`); - } - } finally { - await executeTerminalCommand(`rm -rf ${tempDirPath}`).catch(err => console.error(`删除临时目录失败: ${err.message}`)); - } - } - - async function run_rust_file(params: { file_path: string; cargo_flags?: string }) { - const filePath = params.file_path; - if (!filePath || filePath.trim() === "") { - throw new Error("请提供要执行的 Rust 文件路径"); - } - const fileExistsResult = await executeTerminalCommand(`test -f ${filePath}`); - if (fileExistsResult.exitCode !== 0 || hasError(fileExistsResult.output)) { - throw new Error(`Rust 文件不存在或路径错误: ${filePath}`); - } - - const rustConfig = await ensureRustConfigured(); - if (!rustConfig.success) { - throw new Error(rustConfig.message); - } - - const cargoFlags = params.cargo_flags || "--release"; - const buildMode = cargoFlags.includes("--release") ? "release" : "debug"; - const tempDirPath = "/tmp/temp_rust_project"; - try { - const cargoToml = ` -[package] -name = "temp_rust_script" -version = "0.1.0" -edition = "2021" - -[dependencies] - `; - await executeTerminalCommand(`mkdir -p ${tempDirPath}/src`, 10000); - await executeTerminalCommand(`cat <<'EOF' > ${tempDirPath}/Cargo.toml\n${cargoToml}\nEOF`); - - const readResult = await executeTerminalCommand(`cat ${filePath}`); - if (readResult.exitCode !== 0 || hasError(readResult.output)) { - throw new Error(`无法读取文件: ${filePath}`); - } - const fileContent = readResult.output; - await executeTerminalCommand(`cat <<'EOF' > ${tempDirPath}/src/main.rs\n${fileContent}\nEOF`); - - const compileResult = await executeTerminalCommand(`cd ${tempDirPath} && ${CARGO_MIRROR_ENV} && cargo build ${cargoFlags}`); - if (compileResult.exitCode !== 0 || hasError(compileResult.output)) { - throw new Error(`Rust 文件编译失败:\n${compileResult.output}`); - } - - const execPath = `${tempDirPath}/target/${buildMode}/temp_rust_script`; - const result = await executeTerminalCommand(execPath, 30000); - if (result.exitCode === 0 && !hasError(result.output)) { - return result.output.trim(); - } else { - throw new Error(`Rust 项目执行失败:\n${result.output}`); - } - } finally { - await executeTerminalCommand(`rm -rf ${tempDirPath}`).catch(err => console.error(`删除临时目录失败: ${err.message}`)); - } - } - - async function run_c(params: { script: string; compile_flags?: string }) { - const script = params.script; - if (!script || script.trim() === "") { - throw new Error("请提供要执行的 C 代码内容"); - } - - const compileFlags = params.compile_flags || "-O3 -march=native -fopenmp"; - const tempFilePath = "/tmp/temp_script.c"; - const tempExecPath = "/tmp/temp_script_c_exec"; - try { - await executeTerminalCommand(`cat <<'EOF' > ${tempFilePath}\n${script}\nEOF`); - - const compileResult = await executeTerminalCommand(`gcc ${compileFlags} ${tempFilePath} -o ${tempExecPath}`); - if (compileResult.exitCode !== 0 || hasError(compileResult.output)) { - throw new Error(`C 代码编译失败:\n${compileResult.output}`); - } - - const result = await executeTerminalCommand(tempExecPath); - if (result.exitCode === 0 && !hasError(result.output)) { - return result.output.trim(); - } else { - throw new Error(`C 代码执行失败:\n${result.output}`); - } - } finally { - await executeTerminalCommand(`rm -f ${tempFilePath} ${tempExecPath}`).catch(err => console.error(`删除临时文件失败: ${err.message}`)); - } - } - - async function run_c_file(params: { file_path: string; compile_flags?: string }) { - const filePath = params.file_path; - if (!filePath || filePath.trim() === "") { - throw new Error("请提供要执行的 C 文件路径"); - } - - const fileExistsResult = await executeTerminalCommand(`test -f ${filePath}`); - if (fileExistsResult.exitCode !== 0 || hasError(fileExistsResult.output)) { - throw new Error(`C 文件不存在或路径错误: ${filePath}`); - } - - const compileFlags = params.compile_flags || "-O3 -march=native -fopenmp"; - const tempExecPath = "/tmp/temp_c_exec"; - try { - const compileResult = await executeTerminalCommand(`gcc ${compileFlags} ${filePath} -o ${tempExecPath}`); - if (compileResult.exitCode !== 0 || hasError(compileResult.output)) { - throw new Error(`C 文件编译失败:\n${compileResult.output}`); - } - - const result = await executeTerminalCommand(tempExecPath); - if (result.exitCode === 0 && !hasError(result.output)) { - return result.output.trim(); - } else { - throw new Error(`C 文件执行失败:\n${result.output}`); - } - } finally { - await executeTerminalCommand(`rm -f ${tempExecPath}`).catch(err => console.error(`删除临时文件失败: ${err.message}`)); - } - } - - async function run_cpp(params: { script: string; compile_flags?: string }) { - const script = params.script; - if (!script || script.trim() === "") { - throw new Error("请提供要执行的 C++ 代码内容"); - } - - const compileFlags = params.compile_flags || "-O3 -march=native -fopenmp"; - const tempFilePath = "/tmp/temp_script.cpp"; - const tempExecPath = "/tmp/temp_script_cpp_exec"; - try { - await executeTerminalCommand(`cat <<'EOF' > ${tempFilePath}\n${script}\nEOF`); - - const compileResult = await executeTerminalCommand(`g++ ${compileFlags} ${tempFilePath} -o ${tempExecPath}`); - if (compileResult.exitCode !== 0 || hasError(compileResult.output)) { - throw new Error(`C++ 代码编译失败:\n${compileResult.output}`); - } - - const result = await executeTerminalCommand(tempExecPath); - if (result.exitCode === 0 && !hasError(result.output)) { - return result.output.trim(); - } else { - throw new Error(`C++ 代码执行失败:\n${result.output}`); - } - } finally { - await executeTerminalCommand(`rm -f ${tempFilePath} ${tempExecPath}`).catch(err => console.error(`删除临时文件失败: ${err.message}`)); - } - } - - async function run_cpp_file(params: { file_path: string; compile_flags?: string }) { - const filePath = params.file_path; - if (!filePath || filePath.trim() === "") { - throw new Error("请提供要执行的 C++ 文件路径"); - } - - const fileExistsResult = await executeTerminalCommand(`test -f ${filePath}`); - if (fileExistsResult.exitCode !== 0 || hasError(fileExistsResult.output)) { - throw new Error(`C++ 文件不存在或路径错误: ${filePath}`); - } - - const compileFlags = params.compile_flags || "-O3 -march=native -fopenmp"; - const tempExecPath = "/tmp/temp_cpp_exec"; - try { - const compileResult = await executeTerminalCommand(`g++ ${compileFlags} ${filePath} -o ${tempExecPath}`); - if (compileResult.exitCode !== 0 || hasError(compileResult.output)) { - throw new Error(`C++ 文件编译失败:\n${compileResult.output}`); - } - - const result = await executeTerminalCommand(tempExecPath); - if (result.exitCode === 0 && !hasError(result.output)) { - return result.output.trim(); - } else { - throw new Error(`C++ 文件执行失败:\n${result.output}`); - } - } finally { - await executeTerminalCommand(`rm -f ${tempExecPath}`).catch(err => console.error(`删除临时文件失败: ${err.message}`)); - } - } - - function wrap(func: (params: any) => Promise) { - return async (params: any) => { - try { - const result = await func(params); - complete({ - success: true, - data: result, - }); - } catch (error: any) { - complete({ - success: false, - message: error.message, - error_stack: error.stack, - }); - } - }; - } - - return { - main, - run_javascript_es5, - run_javascript_file, - run_javascript_node, - run_javascript_node_file, - install_node_packages, - install_python_packages, - run_python, - run_python_file, - run_ruby, - run_ruby_file, - run_go, - run_go_file, - run_rust, - run_rust_file, - run_c, - run_c_file, - run_cpp, - run_cpp_file, - wrap - }; -})(); - -// 逐个导出 -exports.main = codeRunner.wrap(codeRunner.main); -exports.run_javascript_es5 = codeRunner.wrap(codeRunner.run_javascript_es5); -exports.run_javascript_file = codeRunner.wrap(codeRunner.run_javascript_file); -exports.run_javascript_node = codeRunner.wrap(codeRunner.run_javascript_node); -exports.run_javascript_node_file = codeRunner.wrap(codeRunner.run_javascript_node_file); -exports.install_node_packages = codeRunner.wrap(codeRunner.install_node_packages); -exports.install_python_packages = codeRunner.wrap(codeRunner.install_python_packages); -exports.run_python = codeRunner.wrap(codeRunner.run_python); -exports.run_python_file = codeRunner.wrap(codeRunner.run_python_file); -exports.run_ruby = codeRunner.wrap(codeRunner.run_ruby); -exports.run_ruby_file = codeRunner.wrap(codeRunner.run_ruby_file); -exports.run_go = codeRunner.wrap(codeRunner.run_go); -exports.run_go_file = codeRunner.wrap(codeRunner.run_go_file); -exports.run_rust = codeRunner.wrap(codeRunner.run_rust); -exports.run_rust_file = codeRunner.wrap(codeRunner.run_rust_file); -exports.run_c = codeRunner.wrap(codeRunner.run_c); -exports.run_c_file = codeRunner.wrap(codeRunner.run_c_file); -exports.run_cpp = codeRunner.wrap(codeRunner.run_cpp); -exports.run_cpp_file = codeRunner.wrap(codeRunner.run_cpp_file); diff --git a/scripts/crossref.ts b/scripts/crossref.ts deleted file mode 100644 index 0c7532b..0000000 --- a/scripts/crossref.ts +++ /dev/null @@ -1,607 +0,0 @@ -/* METADATA -{ - "name": "crossref", - "display_name": { - "zh": "Crossref 学术文献查询", - "en": "Crossref Academic Literature Search" - }, - "description": { - "zh": "Crossref 学术文献查询工具,提供 DOI 查询、关键词搜索、作者搜索等功能,帮助用户查找和获取学术文章元数据。", - "en": "Crossref scholarly literature search tools: query by DOI, keyword, author, title, ISSN, and retrieve publication metadata." - }, - "category": "Search", - "enabledByDefault": true, - "tools": [ - { - "name": "search_by_doi", - "description": { "zh": "通过 DOI (Digital Object Identifier) 查询文章的详细信息", "en": "Query article details by DOI (Digital Object Identifier)." }, - "parameters": [ - { - "name": "doi", - "description": { "zh": "文章的 DOI 标识符,例如 '10.1038/nature12373'", "en": "DOI identifier, e.g. '10.1038/nature12373'" }, - "type": "string", - "required": true - } - ] - }, - { - "name": "search_by_keyword", - "description": { "zh": "通过关键词搜索学术文章", "en": "Search scholarly articles by keyword." }, - "parameters": [ - { - "name": "query", - "description": { "zh": "搜索关键词", "en": "Search query keyword(s)" }, - "type": "string", - "required": true - }, - { - "name": "rows", - "description": { "zh": "返回结果数量,默认 10,最大 100", "en": "Number of results to return (default: 10, max: 100)" }, - "type": "number", - "required": false - }, - { - "name": "sort", - "description": { "zh": "排序方式,可选值:'relevance'(相关性), 'score'(评分), 'updated'(更新时间), 'deposited'(提交时间), 'indexed'(索引时间), 'published'(发布时间)", "en": "Sort mode. Options: 'relevance', 'score', 'updated', 'deposited', 'indexed', 'published'." }, - "type": "string", - "required": false - }, - { - "name": "order", - "description": { "zh": "排序顺序,可选值:'asc'(升序), 'desc'(降序),默认 'desc'", "en": "Sort order: 'asc' or 'desc' (default: 'desc')." }, - "type": "string", - "required": false - } - ] - }, - { - "name": "search_by_author", - "description": { "zh": "通过作者名字搜索文章", "en": "Search articles by author name." }, - "parameters": [ - { - "name": "author", - "description": { "zh": "作者名字", "en": "Author name" }, - "type": "string", - "required": true - }, - { - "name": "rows", - "description": { "zh": "返回结果数量,默认 10,最大 100", "en": "Number of results to return (default: 10, max: 100)" }, - "type": "number", - "required": false - } - ] - }, - { - "name": "search_by_title", - "description": { "zh": "通过文章标题搜索", "en": "Search articles by title." }, - "parameters": [ - { - "name": "title", - "description": { "zh": "文章标题或标题关键词", "en": "Article title or title keyword(s)" }, - "type": "string", - "required": true - }, - { - "name": "rows", - "description": { "zh": "返回结果数量,默认 10,最大 100", "en": "Number of results to return (default: 10, max: 100)" }, - "type": "number", - "required": false - } - ] - }, - { - "name": "search_by_issn", - "description": { "zh": "通过期刊 ISSN 查询该期刊发表的文章", "en": "Search articles published in a journal by ISSN." }, - "parameters": [ - { - "name": "issn", - "description": { "zh": "期刊的 ISSN 标识符,例如 '1476-4687'", "en": "Journal ISSN identifier, e.g. '1476-4687'" }, - "type": "string", - "required": true - }, - { - "name": "rows", - "description": { "zh": "返回结果数量,默认 10,最大 100", "en": "Number of results to return (default: 10, max: 100)" }, - "type": "number", - "required": false - } - ] - } - ] -}*/ -const CrossrefSearch = (function () { - const BASE_URL = "https://api.crossref.org"; - const DEFAULT_ROWS = 10; - const MAX_ROWS = 100; - - function buildQueryString(params: Record): string { - return Object.entries(params) - .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) - .join("&"); - } - - type SearchByDoiParams = { - doi: string; - }; - - type SearchByKeywordParams = { - query: string; - rows?: number; - sort?: string; - order?: string; - }; - - type SearchByAuthorParams = { - author: string; - rows?: number; - }; - - type SearchByTitleParams = { - title: string; - rows?: number; - }; - - type SearchByISSNParams = { - issn: string; - rows?: number; - }; - - /** - * 格式化作者信息 - */ - function formatAuthors(authors: any[]): string { - if (!authors || authors.length === 0) return "N/A"; - return authors - .slice(0, 5) // 只显示前5个作者 - .map((author: any) => { - const given = author.given || ""; - const family = author.family || ""; - return `${given} ${family}`.trim(); - }) - .filter(name => name.length > 0) - .join(", "); - } - - /** - * 格式化日期 - */ - function formatDate(dateParts: number[][]): string { - if (!dateParts || dateParts.length === 0 || !dateParts[0]) return "N/A"; - const parts = dateParts[0]; - if (parts.length === 1) return `${parts[0]}`; - if (parts.length === 2) return `${parts[0]}-${String(parts[1]).padStart(2, '0')}`; - if (parts.length === 3) return `${parts[0]}-${String(parts[1]).padStart(2, '0')}-${String(parts[2]).padStart(2, '0')}`; - return "N/A"; - } - - /** - * 格式化单篇文章信息 - */ - function formatArticle(item: any, index?: number): string { - const lines: string[] = []; - - if (index !== undefined) { - lines.push(`\n=== Article ${index + 1} ===`); - } - - // 标题 - const title = item.title && item.title.length > 0 ? item.title[0] : "No Title"; - lines.push(`Title: ${title}`); - - // DOI - if (item.DOI) { - lines.push(`DOI: ${item.DOI}`); - lines.push(`URL: https://doi.org/${item.DOI}`); - } - - // 作者 - const authors = formatAuthors(item.author); - lines.push(`Authors: ${authors}`); - - // 发表日期 - const publishedDate = formatDate(item.published?.['date-parts'] || item['published-print']?.['date-parts'] || item['published-online']?.['date-parts']); - lines.push(`Published: ${publishedDate}`); - - // 期刊/会议 - if (item['container-title'] && item['container-title'].length > 0) { - lines.push(`Journal/Conference: ${item['container-title'][0]}`); - } - - // ISSN - if (item.ISSN && item.ISSN.length > 0) { - lines.push(`ISSN: ${item.ISSN.join(', ')}`); - } - - // 出版商 - if (item.publisher) { - lines.push(`Publisher: ${item.publisher}`); - } - - // 类型 - if (item.type) { - lines.push(`Type: ${item.type}`); - } - - // 引用次数 - if (item['is-referenced-by-count'] !== undefined) { - lines.push(`Citations: ${item['is-referenced-by-count']}`); - } - - // 摘要(如果有) - if (item.abstract) { - // 移除 HTML 标签 - const abstractText = item.abstract.replace(/<[^>]*>/g, ''); - lines.push(`Abstract: ${abstractText.substring(0, 500)}${abstractText.length > 500 ? '...' : ''}`); - } - - return lines.join('\n'); - } - - /** - * 通过 DOI 查询文章 - */ - async function searchByDoi(params: SearchByDoiParams): Promise { - const { doi } = params; - - if (!doi || doi.trim() === "") { - return { - success: false, - message: "请提供有效的 DOI" - }; - } - - try { - const url = `${BASE_URL}/works/${encodeURIComponent(doi)}`; - const client = OkHttp.newClient(); - const response = await client.get(url, { - 'User-Agent': 'Operit/1.0 (mailto:support@example.com)' - }); - - if (!response.isSuccessful()) { - return { - success: false, - message: `查询失败: HTTP ${response.statusCode} - ${response.statusMessage}` - }; - } - - const data = response.json(); - - if (data.status === "ok" && data.message) { - const article = formatArticle(data.message); - return { - success: true, - message: "查询成功", - data: article - }; - } else { - return { - success: false, - message: "未找到该 DOI 对应的文章" - }; - } - } catch (error: any) { - return { - success: false, - message: `查询失败: ${error.message}` - }; - } - } - - /** - * 通过关键词搜索文章 - */ - async function searchByKeyword(params: SearchByKeywordParams): Promise { - const { query, rows = DEFAULT_ROWS, sort = "relevance", order = "desc" } = params; - - if (!query || query.trim() === "") { - return { - success: false, - message: "请提供有效的搜索关键词" - }; - } - - const actualRows = Math.min(Math.max(rows, 1), MAX_ROWS); - - try { - const queryString = buildQueryString({ - query: query, - rows: String(actualRows), - sort: sort, - order: order - }); - - const url = `${BASE_URL}/works?${queryString}`; - const client = OkHttp.newClient(); - const response = await client.get(url, { - 'User-Agent': 'Operit/1.0 (mailto:support@example.com)' - }); - - if (!response.isSuccessful()) { - return { - success: false, - message: `搜索失败: HTTP ${response.statusCode} - ${response.statusMessage}` - }; - } - - const data = response.json(); - - if (data.status === "ok" && data.message && data.message.items) { - const items = data.message.items; - const totalResults = data.message['total-results']; - - const results = items.map((item: any, index: number) => formatArticle(item, index)); - - const summary = `Found ${totalResults} results (showing ${items.length}):\n${results.join('\n\n')}`; - - return { - success: true, - message: "搜索成功", - data: summary, - total: totalResults, - count: items.length - }; - } else { - return { - success: false, - message: "未找到相关文章" - }; - } - } catch (error: any) { - return { - success: false, - message: `搜索失败: ${error.message}` - }; - } - } - - /** - * 通过作者搜索文章 - */ - async function searchByAuthor(params: SearchByAuthorParams): Promise { - const { author, rows = DEFAULT_ROWS } = params; - - if (!author || author.trim() === "") { - return { - success: false, - message: "请提供有效的作者名字" - }; - } - - const actualRows = Math.min(Math.max(rows, 1), MAX_ROWS); - - try { - const queryString = buildQueryString({ - 'query.author': author, - rows: String(actualRows) - }); - - const url = `${BASE_URL}/works?${queryString}`; - const client = OkHttp.newClient(); - const response = await client.get(url, { - 'User-Agent': 'Operit/1.0 (mailto:support@example.com)' - }); - - if (!response.isSuccessful()) { - return { - success: false, - message: `搜索失败: HTTP ${response.statusCode} - ${response.statusMessage}` - }; - } - - const data = response.json(); - - if (data.status === "ok" && data.message && data.message.items) { - const items = data.message.items; - const totalResults = data.message['total-results']; - - const results = items.map((item: any, index: number) => formatArticle(item, index)); - - const summary = `Found ${totalResults} results for author "${author}" (showing ${items.length}):\n${results.join('\n\n')}`; - - return { - success: true, - message: "搜索成功", - data: summary, - total: totalResults, - count: items.length - }; - } else { - return { - success: false, - message: `未找到该作者 "${author}" 的文章` - }; - } - } catch (error: any) { - return { - success: false, - message: `搜索失败: ${error.message}` - }; - } - } - - /** - * 通过标题搜索文章 - */ - async function searchByTitle(params: SearchByTitleParams): Promise { - const { title, rows = DEFAULT_ROWS } = params; - - if (!title || title.trim() === "") { - return { - success: false, - message: "请提供有效的文章标题" - }; - } - - const actualRows = Math.min(Math.max(rows, 1), MAX_ROWS); - - try { - const queryString = buildQueryString({ - 'query.title': title, - rows: String(actualRows) - }); - - const url = `${BASE_URL}/works?${queryString}`; - const client = OkHttp.newClient(); - const response = await client.get(url, { - 'User-Agent': 'Operit/1.0 (mailto:support@example.com)' - }); - - - const data = response.json(); - - if (data.status === "ok" && data.message && data.message.items) { - const items = data.message.items; - const totalResults = data.message['total-results']; - - const results = items.map((item: any, index: number) => formatArticle(item, index)); - - const summary = `Found ${totalResults} results matching title "${title}" (showing ${items.length}):\n${results.join('\n\n')}`; - - return { - success: true, - message: "搜索成功", - data: summary, - total: totalResults, - count: items.length - }; - } else { - return { - success: false, - message: `未找到标题包含 "${title}" 的文章` - }; - } - } catch (error: any) { - return { - success: false, - message: `搜索失败: ${error.message}` - }; - } - } - - /** - * 通过 ISSN 查询期刊文章 - */ - async function searchByISSN(params: SearchByISSNParams): Promise { - const { issn, rows = DEFAULT_ROWS } = params; - - if (!issn || issn.trim() === "") { - return { - success: false, - message: "请提供有效的 ISSN" - }; - } - - const actualRows = Math.min(Math.max(rows, 1), MAX_ROWS); - - try { - const url = `${BASE_URL}/journals/${encodeURIComponent(issn)}/works?rows=${actualRows}`; - const client = OkHttp.newClient(); - const response = await client.get(url, { - 'User-Agent': 'Operit/1.0 (mailto:support@example.com)' - }); - - if (!response.isSuccessful()) { - return { - success: false, - message: `查询失败: HTTP ${response.statusCode} - ${response.statusMessage}` - }; - } - - const data = response.json(); - - if (data.status === "ok" && data.message && data.message.items) { - const items = data.message.items; - const totalResults = data.message['total-results']; - - const results = items.map((item: any, index: number) => formatArticle(item, index)); - - const summary = `Found ${totalResults} articles from journal ISSN ${issn} (showing ${items.length}):\n${results.join('\n\n')}`; - - return { - success: true, - message: "查询成功", - data: summary, - total: totalResults, - count: items.length - }; - } else { - return { - success: false, - message: `未找到 ISSN "${issn}" 对应期刊的文章` - }; - } - } catch (error: any) { - return { - success: false, - message: `查询失败: ${error.message}` - }; - } - } - - /** - * 包装函数,统一处理错误 - */ - async function wrapToolExecution(func: (params: any) => Promise, params: any) { - try { - const result = await func(params); - complete(result); - } catch (error: any) { - console.error(`工具执行失败`, error); - complete({ - success: false, - message: `工具执行时发生意外错误: ${error.message}`, - }); - } - } - - /** - * 测试函数 - */ - async function main() { - console.log("=== Crossref API 测试 ===\n"); - - // 测试 1: 通过 DOI 查询 - console.log("1. 测试通过 DOI 查询..."); - const doiResult = await searchByDoi({ doi: "10.1038/nature12373" }); - console.log(JSON.stringify(doiResult, null, 2)); - console.log("\n"); - - // 测试 2: 通过关键词搜索 - console.log("2. 测试通过关键词搜索..."); - const keywordResult = await searchByKeyword({ query: "machine learning", rows: 3 }); - console.log(JSON.stringify(keywordResult, null, 2)); - console.log("\n"); - - // 测试 3: 通过作者搜索 - console.log("3. 测试通过作者搜索..."); - const authorResult = await searchByAuthor({ author: "John Smith", rows: 3 }); - console.log(JSON.stringify(authorResult, null, 2)); - console.log("\n"); - - // 测试 4: 通过标题搜索 - console.log("4. 测试通过标题搜索..."); - const titleResult = await searchByTitle({ title: "neural networks", rows: 3 }); - console.log(JSON.stringify(titleResult, null, 2)); - console.log("\n"); - - console.log("=== 测试完成 ==="); - } - - return { - search_by_doi: (params: any) => wrapToolExecution(searchByDoi, params), - search_by_keyword: (params: any) => wrapToolExecution(searchByKeyword, params), - search_by_author: (params: any) => wrapToolExecution(searchByAuthor, params), - search_by_title: (params: any) => wrapToolExecution(searchByTitle, params), - search_by_issn: (params: any) => wrapToolExecution(searchByISSN, params), - main, - }; -})(); - -// 导出工具函数 -exports.search_by_doi = CrossrefSearch.search_by_doi; -exports.search_by_keyword = CrossrefSearch.search_by_keyword; -exports.search_by_author = CrossrefSearch.search_by_author; -exports.search_by_title = CrossrefSearch.search_by_title; -exports.search_by_issn = CrossrefSearch.search_by_issn; -exports.main = CrossrefSearch.main; diff --git a/scripts/daily_life.ts b/scripts/daily_life.ts deleted file mode 100644 index 9aed6b8..0000000 --- a/scripts/daily_life.ts +++ /dev/null @@ -1,1655 +0,0 @@ -/* -METADATA -{ - "name": "daily_life", - - "display_name": { - "zh": "日常生活工具包", - "en": "Daily Life Toolkit" - }, - "description": { "zh": "日常生活工具集合:日期时间、设备状态、电量/内存概况、天气查询、提醒、闹钟、短信、电话、微信单条消息发送、QQ单条消息发送、朋友圈发布、手电筒、音量调节、Wi‑Fi 开关、截图、拍照、深色模式、指定时间唤醒 AI 执行一次性定时任务。", "en": "Daily life utilities: date/time, device status, battery/memory overview, weather lookup, reminders, alarms, SMS, calls, single-message WeChat send, single-message QQ send, Moments posting, flashlight, volume control, Wi‑Fi toggle, screenshots, photos, dark mode, wake AI at a specified time for a one-time scheduled task." }, - "enabledByDefault": true, - "category": "Life", - "tools": [ - { - "name": "get_current_date", - "description": { "zh": "获取当前日期和时间,支持多种格式展示", "en": "Get the current date and time in various formats." }, - "parameters": [ - { - "name": "format", - "description": { "zh": "日期格式('short'简短格式, 'medium'中等格式, 'long'完整格式,或自定义格式)", "en": "Date format: 'short', 'medium', 'long', or a custom format." }, - "type": "string", - "required": false - } - ] - }, - { - "name": "device_status", - "description": { "zh": "获取设备状态信息,包括电池和内存使用情况", "en": "Get device status information, including battery and memory usage." }, - "parameters": [] - }, - { - "name": "set_reminder", - "description": { "zh": "创建提醒或待办事项。", "en": "Create a reminder or to-do item." }, - "parameters": [ - { - "name": "title", - "description": { "zh": "提醒或待办事项的标题", "en": "Title of the reminder or to-do item." }, - "type": "string", - "required": true - }, - { - "name": "description", - "description": { "zh": "提醒的附加详细信息", "en": "Additional details for the reminder (optional)." }, - "type": "string", - "required": false - }, - { - "name": "due_date", - "description": { "zh": "提醒的到期日期(ISO字符串格式)", "en": "Due date of the reminder (ISO string)." }, - "type": "string", - "required": false - } - ] - }, - { - "name": "schedule_one_time_task", - "description": { "zh": "在指定时间唤醒 AI 执行一次性定时任务(chat/role/sender 默认使用 getChatId/getCallerCardId/getCallerName)。", "en": "Wake the AI at a specified time to execute a one-time scheduled task (chat/role/sender default to getChatId/getCallerCardId/getCallerName)." }, - "parameters": [ - { - "name": "trigger_time", - "description": { "zh": "触发时间(格式示例,非当前时间:2026-01-04 10:30、2026-01-04 10:30:00、2026-01-04T10:30:00、2026-01-04)", "en": "Trigger time (format examples, not current time: 2026-01-04 10:30, 2026-01-04 10:30:00, 2026-01-04T10:30:00, 2026-01-04)." }, - "type": "string", - "required": true - }, - { - "name": "message", - "description": { "zh": "唤醒消息内容(可选)", "en": "Wake message content (optional)." }, - "type": "string", - "required": false - }, - ] - }, - { - "name": "set_alarm", - "description": { "zh": "在设备上设置闹钟。", "en": "Set an alarm on the device." }, - "parameters": [ - { - "name": "hour", - "description": { "zh": "闹钟小时(0-23)", "en": "Alarm hour (0-23)." }, - "type": "number", - "required": true - }, - { - "name": "minute", - "description": { "zh": "闹钟分钟(0-59)", "en": "Alarm minute (0-59)." }, - "type": "number", - "required": true - }, - { - "name": "message", - "description": { "zh": "闹钟标签", "en": "Alarm label." }, - "type": "string", - "required": true - }, - { - "name": "days", - "description": { "zh": "重复闹钟的天数(数字数组,1=周日,7=周六)", "en": "Repeat days for the alarm (number array, 1=Sunday, 7=Saturday)." }, - "type": "array", - "required": false - } - ] - }, - { - "name": "send_message", - "description": { "zh": "发送短信", "en": "Send an SMS message." }, - "parameters": [ - { - "name": "phone_number", - "description": { "zh": "接收者电话号码", "en": "Recipient phone number." }, - "type": "string", - "required": true - }, - { - "name": "message", - "description": { "zh": "短信内容", "en": "SMS message content." }, - "type": "string", - "required": true - } - ] - }, - { - "name": "wechat_send_message", - "description": { "zh": "通过微信发送文本消息,调起微信分享界面,由用户选择联系人并确认发送。", "en": "Send a text message via WeChat by opening the share UI; the user selects the contact and confirms sending." }, - "parameters": [ - { - "name": "message", - "description": { "zh": "要发送的文本内容", "en": "Text content to send." }, - "type": "string", - "required": true - } - ] - }, - { - "name": "qq_send_message", - "description": { "zh": "通过QQ发送文本消息,调起QQ分享界面,由用户选择联系人并确认发送。", "en": "Send a text message via QQ by opening the share UI; the user selects the contact and confirms sending." }, - "parameters": [ - { - "name": "message", - "description": { "zh": "要发送的文本内容", "en": "Text content to send." }, - "type": "string", - "required": true - } - ] - }, - { - "name": "wechat_post_moments", - "description": { "zh": "通过微信朋友圈发表文本内容,调起朋友圈编辑界面并预填文案,由用户确认后发送。", "en": "Post text to WeChat Moments by opening the editor with prefilled content; the user confirms and posts." }, - "parameters": [ - { - "name": "message", - "description": { "zh": "要发布到朋友圈的文本内容", "en": "Text content to post to Moments." }, - "type": "string", - "required": true - } - ] - }, - { - "name": "make_phone_call", - "description": { "zh": "拨打电话", "en": "Make a phone call." }, - "parameters": [ - { - "name": "phone_number", - "description": { "zh": "要拨打的电话号码", "en": "Phone number to dial." }, - "type": "string", - "required": true - }, - { - "name": "emergency", - "description": { "zh": "是否为紧急呼叫", "en": "Whether this is an emergency call." }, - "type": "boolean", - "required": false - } - ] - }, - { - "name": "search_weather", - "description": { "zh": "搜索当前天气信息", "en": "Search current weather information." }, - "parameters": [ - { - "name": "location", - "description": { "zh": "要查询天气的位置(城市名称或'current'表示当前位置)", "en": "Location to query (city name or 'current' for current location)." }, - "type": "string", - "required": false - } - ] - }, - { - "name": "toggle_flashlight", - "description": { "zh": "打开或关闭手电筒", "en": "Turn the flashlight on or off." }, - "parameters": [ - { - "name": "state", - "description": { "zh": "手电筒状态:'on'表示打开,'off'表示关闭", "en": "Flashlight state: 'on' to turn on, 'off' to turn off." }, - "type": "string", - "required": true - } - ] - }, - { - "name": "adjust_volume", - "description": { "zh": "调节设备音量,通过模拟按键点击实现", "en": "Adjust device volume by simulating key presses." }, - "parameters": [ - { - "name": "action", - "description": { "zh": "音量调节动作:'up'增加音量,'down'减小音量,'mute'静音", "en": "Volume action: 'up' to increase, 'down' to decrease, 'mute' to mute." }, - "type": "string", - "required": true - }, - { - "name": "count", - "description": { "zh": "按键次数,默认为1次", "en": "Number of key presses (default: 1)." }, - "type": "number", - "required": false - } - ] - }, - { - "name": "toggle_wifi", - "description": { "zh": "打开或关闭Wi-Fi", "en": "Turn Wi-Fi on or off." }, - "parameters": [ - { - "name": "state", - "description": { "zh": "Wi-Fi状态:'on'表示打开,'off'表示关闭", "en": "Wi-Fi state: 'on' to enable, 'off' to disable." }, - "type": "string", - "required": true - } - ] - }, - { - "name": "take_screenshot", - "description": { "zh": "截取当前屏幕", "en": "Take a screenshot of the current screen." }, - "parameters": [ - { - "name": "file_path", - "description": { "zh": "截图保存路径,例如 /sdcard/Pictures/screenshot.png。如果未提供,将使用默认路径和时间戳文件名。", "en": "Screenshot save path, e.g. /sdcard/Pictures/screenshot.png. If omitted, a default path with a timestamp filename will be used." }, - "type": "string", - "required": false - } - ] - }, - { - "name": "take_photo", - "description": { "zh": "打开相机应用拍照", "en": "Open the camera app to take a photo." }, - "parameters": [] - }, - { - "name": "toggle_dark_mode", - "description": { "zh": "切换系统深夜模式(暗色主题)", "en": "Toggle system dark mode (dark theme)." }, - "parameters": [ - { - "name": "state", - "description": { "zh": "模式:'on'开启, 'off'关闭, 'auto'自动", "en": "Mode: 'on' to enable, 'off' to disable, 'auto' for automatic." }, - "type": "string", - "required": true - } - ] - } - ] -}*/ - -const dailyLife = (function () { - /** - * Get the current date and time in various formats - * @param params - Optional parameters including format - */ - async function get_current_date(params: { format?: string }): Promise { - try { - const format = params.format || 'medium'; - const now = new Date(); - - let formattedDate: string; - - switch (format) { - case 'short': - formattedDate = now.toLocaleDateString(); - break; - case 'long': - formattedDate = now.toLocaleDateString(undefined, { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - second: '2-digit' - }); - break; - case 'medium': - default: - formattedDate = now.toLocaleDateString(undefined, { - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - }); - break; - } - - return { - timestamp: now.getTime(), - iso: now.toISOString(), - formatted: formattedDate, - date: { - year: now.getFullYear(), - month: now.getMonth() + 1, - day: now.getDate(), - weekday: now.toLocaleDateString(undefined, { weekday: 'long' }) - }, - time: { - hours: now.getHours(), - minutes: now.getMinutes(), - seconds: now.getSeconds() - } - }; - } catch (error) { - console.error(`[get_current_date] 错误: ${error.message}`); - console.error(error.stack); - throw error; - } - } - - /** - * Get device status information - */ - async function device_status(): Promise { - try { - // Get device information using the System tool - const deviceInfo = await Tools.System.getDeviceInfo(); - - return { - battery: { - level: deviceInfo.batteryLevel, - charging: deviceInfo.batteryCharging - }, - memory: { - total: deviceInfo.totalMemory, - available: deviceInfo.availableMemory - }, - storage: { - total: deviceInfo.totalStorage, - available: deviceInfo.availableStorage - }, - device: { - model: deviceInfo.model, - manufacturer: deviceInfo.manufacturer, - androidVersion: deviceInfo.androidVersion - }, - network: deviceInfo.networkType - }; - } catch (error) { - throw new Error(`Failed to get device status: ${error.message}`); - } - } - - /** - * Search for current weather information - * @param params - Parameters with optional location - */ - async function search_weather(params: { location?: string }): Promise { - try { - // Default to "当前天气" if no location is provided - const location = params.location || "current"; - - // 构造搜索URL - const searchUrl = location === "current" ? - "https://www.baidu.com/s?wd=当前天气" : - `https://www.baidu.com/s?wd=${encodeURIComponent(location + " 天气")}`; - - console.log(`搜索天气信息: ${searchUrl}`); - - // 使用visit_web工具直接访问搜索页面 - const result = await Tools.Net.visit(searchUrl); - - // 从网页内容中提取有用的信息 - // 由于结果现在是页面内容而非结构化搜索结果,我们需要提取有用信息 - const extractedInfo = extractWeatherInfo(result.content); - - return { - success: true, - query: location === "current" ? "当前天气" : `${location} 天气`, - location: location, - timestamp: new Date().toISOString(), - url: result.url, - title: result.title, - weather_info: extractedInfo, - note: "天气数据来自网页内容提取,仅供参考。" - }; - } catch (error) { - console.error(`[search_weather] 错误: ${error.message}`); - console.error(error.stack); - - throw new Error(`获取天气信息失败: ${error.message}`); - } - } - - /** - * 从网页内容中提取天气信息 - * @param content - 网页内容 - * @returns 提取的天气信息 - */ - function extractWeatherInfo(content: string): string { - // 直接返回页面内容的一部分,保留更多原始信息 - // 移除一些可能的HTML标签,但不做过度处理 - const cleanContent = content - .replace(/<\/?[^>]+(>|$)/g, " ") // 简单移除HTML标签 - .replace(/\s+/g, " ") // 压缩多余空白 - .trim(); - - // 返回更大篇幅的内容 - const maxLength = 1500; // 返回更多内容 - - return cleanContent.length > maxLength ? - cleanContent.substring(0, maxLength) + "..." : - cleanContent; - } - - /** - * Set a reminder or to-do item - * @param params - Parameters with reminder details - */ - async function set_reminder(params: { title: string; description?: string; due_date?: string }): Promise { - try { - if (!params.title) { - throw new Error("Reminder title is required"); - } - - console.log("创建提醒..."); - console.log("尝试使用隐式Intent创建日历事件..."); - - // 创建Intent - const intent = new Intent(IntentAction.ACTION_INSERT); - // 设置日历事件的URI - intent.setData("content://com.android.calendar/events"); - - // 设置事件详情 - intent.putExtra("title", params.title); - if (params.description) { - intent.putExtra("description", params.description); - } - - // 处理日期 - if (params.due_date) { - const dueDate = new Date(params.due_date); - // 使用毫秒级时间戳 - const beginTime = dueDate.getTime(); - const endTime = beginTime + 3600000; // 默认1小时后 - intent.putExtra("beginTime", beginTime); - intent.putExtra("endTime", endTime); - - // 添加单独的日期和时间组件,增加兼容性 - intent.putExtra("eventTimezone", "UTC"); - intent.putExtra("allDay", false); - } else { - // 默认使用当前时间后1小时 - const now = new Date(); - const beginTime = now.getTime(); - const endTime = beginTime + 3600000; - intent.putExtra("beginTime", beginTime); - intent.putExtra("endTime", endTime); - - // 添加单独的日期和时间组件 - intent.putExtra("eventTimezone", "UTC"); - intent.putExtra("allDay", false); - } - - // 设置提醒 - intent.putExtra("hasAlarm", 1); - - // 添加标志 - intent.addFlag(IntentFlag.ACTIVITY_NEW_TASK); - - // 启动Intent - const result = await intent.start(); - - // 返回结果 - return { - success: true, - message: "提醒创建成功", - title: params.title, - description: params.description || undefined, - due_date: params.due_date || undefined, - method: "implicit_intent", - raw_result: result - }; - } catch (error) { - console.error(`[set_reminder] 错误: ${error.message}`); - console.error(error.stack); - - return { - success: false, - message: `创建提醒失败: ${error.message}`, - title: params.title, - description: params.description || undefined, - due_date: params.due_date || undefined, - error: error.message - }; - } - } - - /** - * Create a one-time scheduled workflow to send a reminder message to a chat. - */ - async function schedule_one_time_task(params: { - trigger_time: string; - message?: string; - }): Promise { - if (!params.trigger_time) { - throw new Error("trigger_time is required"); - } - - const resolvedChatId = getChatId(); - if (!resolvedChatId) { - throw new Error("chat_id is required (call from a chat context)"); - } - - const resolvedRoleCardId = getCallerCardId(); - const resolvedSenderName = getCallerName(); - const resolvedLang = getLang(); - const workflowName = resolvedLang?.toLowerCase()?.startsWith("zh") - ? `一次性定时任务 ${params.trigger_time}` - : `One-time scheduled task ${params.trigger_time}`; - const description = resolvedLang?.toLowerCase()?.startsWith("zh") - ? `一次性定时任务,触发时间 ${params.trigger_time}` - : `One-time scheduled task at ${params.trigger_time}`; - const timedTriggerTag = `[定时触发:${params.trigger_time}]\n[Scheduled Trigger:${params.trigger_time}]`; - const defaultMessage = `[自动任务] 时间到了,开始执行一次性定时任务。现在是 ${params.trigger_time}\n[Automated Task] Time reached, start executing the one-time scheduled task. Current time is ${params.trigger_time}`; - const messageContent = params.message - ? `${params.message}\n${timedTriggerTag}` - : `${defaultMessage}\n${timedTriggerTag}`; - - const created = await Tools.Workflow.create(workflowName, description, null, null, true); - const workflowId = created?.id; - - if (!workflowId) { - throw new Error("Failed to create workflow: missing workflow id"); - } - - const uniqueId = Date.now().toString(); - const triggerNodeId = `trigger_${uniqueId}`; - const startNodeId = `start_${uniqueId}`; - const sendNodeId = `send_${uniqueId}`; - const deleteNodeId = `delete_${uniqueId}`; - - const sendMessageOptions: NonNullable[4]> = { - hide_user_message: true, - notify_reply: true, - persist_turn: true, - disable_warning: false, - }; - const sendMessageArgs: Parameters = [ - messageContent, - resolvedChatId, - resolvedRoleCardId || undefined, - resolvedSenderName || undefined, - sendMessageOptions - ]; - const [typedMessage, typedChatId, typedRoleCardId, typedSenderName, typedOptions] = sendMessageArgs; - - const actionConfig: Record = { - message: typedMessage, - chat_id: typedChatId, - hide_user_message: true, - notify_reply: true, - persist_turn: true, - disable_warning: false, - }; - - if (typedRoleCardId) { - actionConfig.role_card_id = typedRoleCardId; - } - if (typedSenderName) { - actionConfig.sender_name = typedSenderName; - } - - const nodeTexts = resolvedLang?.toLowerCase()?.startsWith("zh") - ? { - triggerName: "定时触发", - triggerDesc: "一次性定时触发", - startName: "启动对话服务", - startDesc: "启动对话服务以便发送消息", - sendName: "唤醒AI", - sendDesc: "向对话发送唤醒消息,触发AI执行任务", - deleteName: "删除工作流", - deleteDesc: "执行后删除自己" - } - : { - triggerName: "Scheduled Trigger", - triggerDesc: "One-time scheduled trigger", - startName: "Start Chat Service", - startDesc: "Start chat service to enable messaging", - sendName: "Wake AI", - sendDesc: "Send a wake message to trigger AI task execution", - deleteName: "Delete Workflow", - deleteDesc: "Delete itself after execution" - }; - - const nodes: Workflow.NodeInput[] = [ - { - id: triggerNodeId, - type: "trigger", - name: nodeTexts.triggerName, - description: nodeTexts.triggerDesc, - position: { x: 0, y: 0 }, - triggerType: "schedule", - triggerConfig: { - schedule_type: "specific_time", - specific_time: params.trigger_time, - repeat: "false", - enabled: "true" - } - }, - { - id: startNodeId, - type: "execute", - name: nodeTexts.startName, - description: nodeTexts.startDesc, - position: { x: 260, y: 0 }, - actionType: "start_chat_service", - actionConfig: { - initial_mode: "BALL", - keep_if_exists: "true" - } - }, - { - id: sendNodeId, - type: "execute", - name: nodeTexts.sendName, - description: nodeTexts.sendDesc, - position: { x: 520, y: 0 }, - actionType: "send_message_to_ai", - actionConfig - }, - { - id: deleteNodeId, - type: "execute", - name: nodeTexts.deleteName, - description: nodeTexts.deleteDesc, - position: { x: 1040, y: 0 }, - actionType: "delete_workflow", - actionConfig: { - workflow_id: workflowId - } - } - ]; - - const connections: Workflow.ConnectionInput[] = [ - { - sourceNodeId: triggerNodeId, - targetNodeId: startNodeId, - condition: "on_success" - }, - { - sourceNodeId: startNodeId, - targetNodeId: sendNodeId, - condition: "on_success" - }, - { - sourceNodeId: sendNodeId, - targetNodeId: deleteNodeId, - condition: "on_success" - } - ]; - - await Tools.Workflow.update(workflowId, { - name: workflowName, - description, - enabled: true, - nodes, - connections - }); - - return { - success: true, - message: "一次性提醒工作流已创建", - data: { - workflow_id: workflowId, - trigger_time: params.trigger_time, - chat_id: resolvedChatId, - role_card_id: resolvedRoleCardId || undefined, - workflow_name: workflowName - } - }; - } - - /** - * Set an alarm on the device - * @param params - Parameters with alarm details - */ - async function set_alarm(params: { hour: number; minute: number; message: string; days?: number[] }): Promise { - try { - if (params.hour === undefined || params.minute === undefined) { - throw new Error("Hour and minute are required for setting an alarm"); - } - const repeatDays = Array.isArray(params.days) - ? params.days.filter((day): day is number => typeof day === 'number') - : []; - - if (params.hour < 0 || params.hour > 23) { - throw new Error("Hour must be between 0 and 23"); - } - - if (params.minute < 0 || params.minute > 59) { - throw new Error("Minute must be between 0 and 59"); - } - - console.log("设置闹钟..." + params.hour + ":" + params.minute); - console.log("尝试使用隐式Intent设置闹钟..."); - - // 创建Intent - 使用Android的标准闹钟Intent Action - const intent = new Intent("android.intent.action.SET_ALARM"); - // 或者也可以使用常量: new Intent(IntentAction.ACTION_MAIN); - - // 设置闹钟详情 - 使用正确的参数名称 - intent.putExtra("android.intent.extra.alarm.HOUR", params.hour); - intent.putExtra("android.intent.extra.alarm.MINUTES", params.minute); - - // 添加标签 - intent.putExtra("android.intent.extra.alarm.MESSAGE", params.message); - - - // 设置重复日期(如果提供) - if (repeatDays.length > 0) { - intent.putExtra("android.intent.extra.alarm.DAYS", repeatDays); - } - - // 不跳过UI确认 - 这可能是问题所在,某些设备需要用户确认 - // intent.putExtra("android.intent.extra.alarm.SKIP_UI", true); - - // 设置闹钟应该立即响铃 - intent.putExtra("android.intent.extra.alarm.VIBRATE", true); - - // 添加标志 - 必须使用NEW_TASK标志来启动Activity - intent.addFlag(IntentFlag.ACTIVITY_NEW_TASK); - - // 确保Intent被视为Activity启动而不是广播 - // 手动添加DEFAULT类别以确保Intent可以被正确处理 - intent.addCategory("android.intent.category.DEFAULT"); - - // 启动Intent - 这会显示闹钟设置界面 - const result = await intent.start(); - - // 返回结果 - return { - success: true, - message: "闹钟设置成功", - alarm_time: `${params.hour.toString().padStart(2, '0')}:${params.minute.toString().padStart(2, '0')}`, - label: params.message || undefined, - repeat_days: params.days || undefined, - method: "implicit_intent", - raw_result: result - }; - } catch (error) { - console.error(`[set_alarm] 错误: ${error.message}`); - console.error(error.stack); - - return { - success: false, - message: `设置闹钟失败: ${error.message}`, - alarm_time: `${params.hour.toString().padStart(2, '0')}:${params.minute.toString().padStart(2, '0')}`, - label: params.message || undefined, - repeat_days: params.days || undefined, - error: error.message - }; - } - } - - /** - * Send a text message - * @param params - Parameters with message details - */ - async function send_message(params: { phone_number: string; message: string }): Promise { - try { - if (!params.phone_number) { - throw new Error("Phone number is required"); - } - - if (!params.message) { - throw new Error("Message content is required"); - } - - console.log(`发送短信: ${params.phone_number}`); - - // 创建短信Intent - const intent = new Intent(IntentAction.ACTION_SENDTO); - - // 设置短信URI - const smsUri = `smsto:${params.phone_number}`; - intent.setData(smsUri); - - // 添加短信内容 - intent.putExtra("sms_body", params.message); - - // 添加必要的标志 - intent.addFlag(IntentFlag.ACTIVITY_NEW_TASK); - - // 启动Intent - const result = await intent.start(); - - return { - success: true, - message: "短信编辑界面已打开", - phone_number: params.phone_number, - content_preview: params.message.length > 30 ? params.message.substring(0, 30) + "..." : params.message, - raw_result: result - }; - } catch (error) { - console.error(`发送短信失败: ${error.message}`); - return { - success: false, - message: `发送短信失败: ${error.message}`, - phone_number: params.phone_number - }; - } - } - - /** - * Use Intent to share a text message via WeChat - * @param params - Parameters with message content - */ - async function wechat_send_message(params: { message: string }): Promise { - try { - if (!params.message) { - throw new Error("Message content is required"); - } - - console.log("通过 Intent 调起微信发送文本消息..."); - - const intent = new Intent(IntentAction.ACTION_SEND); - intent.setType("text/plain"); - intent.putExtra("android.intent.extra.TEXT", params.message); - intent.setComponent("com.tencent.mm", "com.tencent.mm.ui.tools.ShareImgUI"); - intent.addFlag(IntentFlag.ACTIVITY_NEW_TASK); - - const result = await intent.start(); - - return { - success: true, - message: "已打开微信分享界面,请选择联系人并确认发送", - content_preview: params.message.length > 30 ? params.message.substring(0, 30) + "..." : params.message, - raw_result: result - }; - } catch (error) { - console.error(`[wechat_send_message] 错误: ${error.message}`); - console.error(error.stack); - return { - success: false, - message: `调用微信发送消息失败: ${error.message}`, - content_preview: params.message ? (params.message.length > 30 ? params.message.substring(0, 30) + "..." : params.message) : "" - }; - } - } - - /** - * Use Intent to share a text message via QQ - * @param params - Parameters with message content - */ - async function qq_send_message(params: { message: string }): Promise { - try { - if (!params.message) { - throw new Error("Message content is required"); - } - - console.log("通过 Intent 调起 QQ 发送文本消息..."); - - const intent = new Intent(IntentAction.ACTION_SEND); - intent.setType("text/plain"); - intent.putExtra("android.intent.extra.TEXT", params.message); - intent.setComponent("com.tencent.mobileqq", "com.tencent.mobileqq.activity.JumpActivity"); - intent.addFlag(IntentFlag.ACTIVITY_NEW_TASK); - - const result = await intent.start(); - - return { - success: true, - message: "已打开 QQ 分享界面,请选择联系人并确认发送", - content_preview: params.message.length > 30 ? params.message.substring(0, 30) + "..." : params.message, - raw_result: result - }; - } catch (error) { - console.error(`[qq_send_message] 错误: ${error.message}`); - console.error(error.stack); - return { - success: false, - message: `调用 QQ 发送消息失败: ${error.message}`, - content_preview: params.message ? (params.message.length > 30 ? params.message.substring(0, 30) + "..." : params.message) : "" - }; - } - } - - /** - * Use Intent to open WeChat Moments publish screen with text prefilled - * @param params - Parameters with message content - */ - async function wechat_post_moments(params: { message: string }): Promise { - try { - if (!params.message) { - throw new Error("Message content is required"); - } - - console.log("通过 Intent 调起微信朋友圈发表界面..."); - - const intent = new Intent(IntentAction.ACTION_SEND); - intent.setType("text/plain"); - intent.setComponent("com.tencent.mm", "com.tencent.mm.ui.tools.ShareToTimeLineUI"); - intent.putExtra("android.intent.extra.TEXT", params.message); - intent.putExtra("Kdescription", params.message); - intent.addFlag(IntentFlag.ACTIVITY_NEW_TASK); - - const result = await intent.start(); - - return { - success: true, - message: "已打开微信朋友圈发表界面,请确认内容并发送", - content_preview: params.message.length > 30 ? params.message.substring(0, 30) + "..." : params.message, - raw_result: result - }; - } catch (error) { - console.error(`[wechat_post_moments] 错误: ${error.message}`); - console.error(error.stack); - return { - success: false, - message: `调用微信朋友圈失败: ${error.message}`, - content_preview: params.message ? (params.message.length > 30 ? params.message.substring(0, 30) + "..." : params.message) : "" - }; - } - } - - /** - * Make a phone call - * @param params - Parameters with call details - */ - async function make_phone_call(params: { phone_number: string; emergency?: boolean }): Promise { - try { - if (!params.phone_number) { - throw new Error("Phone number is required"); - } - - console.log(`拨打电话: ${params.phone_number}`); - - // 选择合适的Intent Action - // 如果是紧急电话,使用ACTION_CALL_EMERGENCY,否则使用ACTION_DIAL - const isEmergency = params.emergency === true; - const action = isEmergency ? IntentAction.ACTION_CALL_EMERGENCY : IntentAction.ACTION_DIAL; - - // 创建拨号Intent - const intent = new Intent(action); - - // 设置电话URI - const phoneUri = `tel:${params.phone_number}`; - intent.setData(phoneUri); - - // 添加必要的标志 - intent.addFlag(IntentFlag.ACTIVITY_NEW_TASK); - - // 启动Intent - const result = await intent.start(); - - return { - success: true, - message: isEmergency ? "紧急电话已拨打" : "拨号界面已打开", - phone_number: params.phone_number, - is_emergency: isEmergency, - raw_result: result - }; - } catch (error) { - console.error(`拨打电话失败: ${error.message}`); - return { - success: false, - message: `拨打电话失败: ${error.message}`, - phone_number: params.phone_number, - is_emergency: params.emergency === true - }; - } - } - - /** - * Toggle flashlight on or off - * @param params - Parameters with flashlight state - */ - async function toggle_flashlight(params: { state: string }): Promise { - try { - if (!params.state) { - throw new Error("Flashlight state is required"); - } - - // Normalize state to lowercase - const state = params.state.toLowerCase(); - - if (state !== 'on' && state !== 'off') { - throw new Error("Invalid state. Must be 'on' or 'off'"); - } - - console.log(`${state === 'on' ? '打开' : '关闭'}手电筒...`); - - // Convert state to numeric value (1 for on, 0 for off) - const stateValue = state === 'on' ? '1' : '0'; - - // Set FlashState using Tools.System.setSetting - const flashStateResult = await Tools.System.setSetting('FlashState', stateValue, 'system'); - console.log(`FlashState 设置结果: ${JSON.stringify(flashStateResult)}`); - - // Set back_flashlight_state using Tools.System.setSetting - const backFlashlightResult = await Tools.System.setSetting('back_flashlight_state', stateValue, 'system'); - console.log(`back_flashlight_state 设置结果: ${JSON.stringify(backFlashlightResult)}`); - - return { - success: true, - message: `手电筒已${state === 'on' ? '打开' : '关闭'}`, - state: state, - flash_state_result: flashStateResult, - back_flashlight_result: backFlashlightResult - }; - } catch (error) { - console.error(`手电筒操作失败: ${error.message}`); - return { - success: false, - message: `手电筒操作失败: ${error.message}`, - state: params.state - }; - } - } - - /** - * Adjust device volume using simulated key presses - * @param params - Parameters with volume action and count - */ - async function adjust_volume(params: { action: string; count?: number }): Promise { - try { - if (!params.action) { - throw new Error("Volume action is required"); - } - - // Normalize action to lowercase - const action = params.action.toLowerCase(); - - if (action !== 'up' && action !== 'down' && action !== 'mute') { - throw new Error("Invalid action. Must be 'up', 'down', or 'mute'"); - } - - // Parse count parameter - let count = params.count === undefined ? 1 : params.count; - if (!Number.isFinite(count) || count < 1 || count > 20) { - throw new Error("Count must be a number between 1 and 20"); - } - - console.log(`调节音量: ${action === 'up' ? '增加' : action === 'down' ? '减小' : '静音'}, 次数: ${count}`); - - // Map action to Android key code string - let keyCode: string; - let actionName: string; - - switch (action) { - case 'up': - keyCode = 'KEYCODE_VOLUME_UP'; - actionName = '增加'; - break; - case 'down': - keyCode = 'KEYCODE_VOLUME_DOWN'; - actionName = '减小'; - break; - case 'mute': - keyCode = 'KEYCODE_VOLUME_MUTE'; - actionName = '静音'; - count = 1; // Mute should only be pressed once - break; - default: - throw new Error("Invalid action"); - } - - // Simulate key presses using Tools.UI.pressKey - const results: any[] = []; - for (let i = 0; i < count; i++) { - const result = await Tools.UI.pressKey(keyCode); - results.push(result); - console.log(`第 ${i + 1} 次按键结果: ${JSON.stringify(result)}`); - - // Add a small delay between key presses to make them more natural - if (i < count - 1) { - await sleep(100); - } - } - - return { - success: true, - message: `音量${actionName}操作完成,共执行 ${count} 次`, - action: action, - key_code: keyCode, - count: count, - results: results - }; - } catch (error) { - console.error(`音量调节失败: ${error.message}`); - return { - success: false, - message: `音量调节失败: ${error.message}`, - action: params.action, - count: params.count || 1 - }; - } - } - - /** - * Toggle Wi-Fi on or off - * @param params - Parameters with Wi-Fi state - */ - async function toggle_wifi(params: { state: string }): Promise { - try { - if (!params.state || (params.state.toLowerCase() !== 'on' && params.state.toLowerCase() !== 'off')) { - throw new Error("State is required and must be 'on' or 'off'"); - } - - const state = params.state.toLowerCase(); - console.log(`${state === 'on' ? '开启' : '关闭'} Wi-Fi...`); - - const command = state === 'on' ? 'svc wifi enable' : 'svc wifi disable'; - const result = await Tools.System.shell(command); - - return { - success: true, - message: `Wi-Fi 已${state === 'on' ? '开启' : '关闭'}`, - state: state, - raw_result: result - }; - } catch (error) { - console.error(`Wi-Fi 操作失败: ${error.message}`); - return { - success: false, - message: `Wi-Fi 操作失败: ${error.message}`, - state: params.state - }; - } - } - - /** - * Take a screenshot - * @param params - Optional parameters with file path - */ - async function take_screenshot(params: { file_path?: string }): Promise { - try { - let filePath = params.file_path; - const screenshotDir = "/sdcard/DCIM/Screenshots"; - - // Ensure the directory exists - await Tools.Files.mkdir(screenshotDir, true); - - if (!filePath) { - const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); - filePath = `${screenshotDir}/screenshot_${timestamp}.png`; - } - - console.log(`截取屏幕并保存到: ${filePath}`); - - const result = await Tools.System.shell(`screencap -p ${filePath}`); - - return { - success: true, - message: `截图已保存到 ${filePath}`, - file_path: filePath, - raw_result: result - }; - } catch (error) { - console.error(`截图失败: ${error.message}`); - return { - success: false, - message: `截图失败: ${error.message}` - }; - } - } - - /** - * Open camera to take a photo - */ - async function take_photo(): Promise { - try { - console.log("打开相机应用..."); - - const intent = new Intent(IntentAction.ACTION_IMAGE_CAPTURE); - intent.addFlag(IntentFlag.ACTIVITY_NEW_TASK); - const result = await intent.start(); - - return { - success: true, - message: "相机应用已打开", - raw_result: result - }; - } catch (error) { - console.error(`打开相机失败: ${error.message}`); - return { - success: false, - message: `打开相机失败: ${error.message}` - }; - } - } - - /** - * Toggle dark mode - * @param params - Parameters with dark mode state - */ - async function toggle_dark_mode(params: { state: string }): Promise { - try { - if (!params.state) { - throw new Error("Dark mode state is required"); - } - - const state = params.state.toLowerCase(); - let shellArg: string; - let stateName: string; - - switch (state) { - case 'on': - shellArg = 'yes'; - stateName = '开启'; - break; - case 'off': - shellArg = 'no'; - stateName = '关闭'; - break; - case 'auto': - shellArg = 'auto'; - stateName = '自动'; - break; - default: - throw new Error("Invalid state. Must be 'on', 'off', or 'auto'"); - } - - console.log(`设置深夜模式为 ${stateName}...`); - - const command = `cmd uimode night ${shellArg}`; - const result = await Tools.System.shell(command); - - - return { - success: true, - message: `深夜模式已设置为 ${stateName}`, - state: state, - raw_result: result - }; - } catch (error) { - console.error(`深夜模式操作失败: ${error.message}`); - return { - success: false, - message: `深夜模式操作失败: ${error.message}`, - state: params.state - }; - } - } - - /** - * 等待指定的毫秒数 - * @param ms 等待的毫秒数 - */ - async function sleep(ms: number): Promise { - if (!Number.isFinite(ms) || ms < 0) { - throw new Error("Sleep duration must be a non-negative number"); - } - return new Promise(resolve => setTimeout(resolve, ms)); - } - - /** - * Test and demonstrate all daily life functions - * This function shows examples of all available daily life functions - */ - async function main(): Promise { - try { - // 创建测试结果对象 - 添加接口定义以解决类型错误 - const results: { - date?: any; - device?: any; - weather?: any; - reminder?: any; - alarm?: any; - message?: any; - call?: any; - wifi?: any; - screenshot?: any; - photo?: any; - dark_mode?: any; - } = {}; - - // 1. 测试当前日期时间函数 - // console.log("测试获取当前日期时间..."); - // try { - // const dateResult = await get_current_date({}); - // results.date = dateResult; - // console.log("✓ 日期时间获取成功"); - // } catch (error) { - // results.date = { error: `获取日期时间失败: ${error.message}` }; - // console.log("✗ 日期时间获取失败"); - // } - - // // 2. 测试设备状态函数 - // console.log("测试获取设备状态..."); - // try { - // const deviceResult = await device_status(); - // results.device = deviceResult; - // console.log("✓ 设备状态获取成功"); - // } catch (error) { - // results.device = { error: `获取设备状态失败: ${error.message}` }; - // console.log("✗ 设备状态获取失败"); - // } - - // // 3. 测试天气搜索功能 - // console.log("测试天气搜索..."); - // try { - // const weatherResult = await search_weather({ location: "current" }); - // results.weather = weatherResult; - // console.log("✓ 天气搜索成功"); - // } catch (error) { - // results.weather = { error: `天气搜索失败: ${error.message}` }; - // console.log("✗ 天气搜索失败"); - // } - - // // 4. 测试设置提醒功能 - // // console.log("测试设置提醒..."); - // // try { - // // const reminderResult = await set_reminder({ - // // title: "测试提醒", - // // description: "这是一个测试提醒", - // // // 可以设置未来时间 - // // due_date: new Date(Date.now() + 3600000).toISOString() // 1小时后 - // // }); - // // results.reminder = reminderResult; - // // console.log("✓ 设置提醒成功"); - // // } catch (error) { - // // results.reminder = { error: `设置提醒失败: ${error.message}` }; - // // console.log("✗ 设置提醒失败"); - // // } - - // // 5. 测试设置闹钟功能 - // console.log("测试设置闹钟..."); - // try { - // // 获取当前时间,设置为5分钟后的闹钟 - // const now = new Date(); - // const hour = now.getHours(); - // const minute = (now.getMinutes() + 5) % 60; - - // const alarmResult = await set_alarm({ - // hour: hour.toString(), - // minute: minute.toString(), - // message: "测试闹钟", - // // 可以添加重复日期测试,例如每周一和周五 - // // days: [2, 6] // 2表示周一,6表示周五 - // }); - // results.alarm = alarmResult; - // console.log("✓ 设置闹钟成功"); - // } catch (error) { - // results.alarm = { error: `设置闹钟失败: ${error.message}` }; - // console.log("✗ 设置闹钟失败"); - // } - - // // 6. 测试发送消息功能 - // console.log("测试发送短信功能..."); - // try { - // // 使用测试号码 - 常用的中国运营商服务号码,适合测试 - // const testPhoneNumber = "10086"; - // const testMessage = "这是一条测试短信,不会实际发送"; - - // // 直接调用发送短信功能 - // const smsResult = await send_message({ - // phone_number: testPhoneNumber, - // message: testMessage - // }); - // results.message = smsResult; - // console.log("✓ 短信测试界面打开成功"); - - // // 等待用户查看短信界面 - // await sleep(5000); - // } catch (error) { - // results.message = { error: `短信测试失败: ${error.message}` }; - // console.log("✗ 短信测试失败"); - // } - - // // 7. 测试拨打电话功能 - // console.log("测试拨号功能..."); - // try { - // // 使用测试号码 - 常用的中国运营商服务号码,适合测试 - // const testPhoneNumber = "10086"; - - // // 直接调用拨号功能 - // const dialResult = await make_phone_call({ - // phone_number: testPhoneNumber, - // emergency: false - // }); - // results.call = dialResult; - // console.log("✓ 拨号界面打开成功"); - - // // 等待用户查看拨号界面 - // await sleep(5000); - // } catch (error) { - // results.call = { error: `拨号测试失败: ${error.message}` }; - // console.log("✗ 拨号测试失败"); - // } - - // 8. Test toggling Wi-Fi - // console.log("测试Wi-Fi开关..."); - // try { - // // Turn Wi-Fi off, wait, then turn it on - // await toggle_wifi({ state: 'off' }); - // await sleep(3000); - // const wifiResult = await toggle_wifi({ state: 'on' }); - // results.wifi = wifiResult; - // console.log("✓ Wi-Fi开关测试成功"); - // } catch (error) { - // results.wifi = { error: `Wi-Fi开关测试失败: ${error.message}` }; - // console.log("✗ Wi-Fi开关测试失败"); - // } - - // 9. Test taking a screenshot - // console.log("测试截图功能..."); - // try { - // const screenshotResult = await take_screenshot({}); - // results.screenshot = screenshotResult; - // console.log("✓ 截图成功"); - // } catch (error) { - // results.screenshot = { error: `截图失败: ${error.message}` }; - // console.log("✗ 截图失败"); - // } - - // 10. Test taking a photo - // console.log("测试拍照功能..."); - // try { - // const photoResult = await take_photo(); - // results.photo = photoResult; - // if (photoResult.success) { - // console.log("✓ 打开相机成功"); - // await sleep(5000); // Wait for user to see camera - // } else { - // console.log(`✗ 打开相机失败: ${photoResult.message}`); - // } - // } catch (error) { - // results.photo = { error: `打开相机失败: ${error.message}` }; - // console.log("✗ 打开相机失败"); - // } - - // 11. Test toggling dark mode - // console.log("测试深夜模式切换..."); - // try { - // // Turn dark mode on, then set to auto - // await toggle_dark_mode({ state: 'on' }); - // await sleep(2000); - // const darkModeResult = await toggle_dark_mode({ state: 'auto' }); - // results.dark_mode = darkModeResult; - // console.log("✓ 深夜模式切换成功"); - // } catch (error) { - // results.dark_mode = { error: `深夜模式切换失败: ${error.message}` }; - // console.log("✗ 深夜模式切换失败"); - // } - - // 返回所有测试结果 - return { - message: "日常生活功能测试完成", - test_results: results, - timestamp: new Date().toISOString(), - summary: "测试了各种日常生活功能,包括天气搜索、拨号和短信测试。请查看各功能的测试结果。" - }; - } catch (error) { - return { - success: false, - message: `测试过程中发生错误: ${error.message}` - }; - } - } - - /** - * 包装函数 - 统一处理所有daily_life函数的返回结果 - * @param func 原始函数 - * @param params 函数参数 - * @param successMessage 成功消息 - * @param failMessage 失败消息 - * @param additionalInfo 附加信息(可选) - */ - async function daily_wrap( - func: (params: any) => Promise, - params: any, - successMessage: string, - failMessage: string, - additionalInfo: string = "" - ): Promise { - try { - console.log(`开始执行函数: ${func.name || '匿名函数'}`); - console.log(`参数:`, JSON.stringify(params, undefined, 2)); - - // 执行原始函数 - const result = await func(params); - - console.log(`函数 ${func.name || '匿名函数'} 执行结果:`, JSON.stringify(result, undefined, 2)); - - // 如果原始函数已经调用了complete,就不需要再次调用 - if (result === undefined) return; - - // 根据结果类型处理 - if (typeof result === "boolean") { - // 布尔类型结果 - complete({ - success: result, - message: result ? successMessage : failMessage, - additionalInfo: additionalInfo - }); - } else { - // 数据类型结果 - complete({ - success: true, - message: successMessage, - additionalInfo: additionalInfo, - data: result - }); - } - } catch (error) { - // 详细记录错误信息 - console.error(`函数 ${func.name || '匿名函数'} 执行失败!`); - console.error(`错误信息: ${error.message}`); - console.error(`错误堆栈: ${error.stack}`); - - // 处理错误 - complete({ - success: false, - message: `${failMessage}: ${error.message}`, - additionalInfo: additionalInfo, - error_stack: error.stack - }); - } - } - - return { - get_current_date: async (params) => await daily_wrap( - get_current_date, - params, - "获取日期时间成功", - "获取日期时间失败" - ), - device_status: async (params) => await daily_wrap( - device_status, - params, - "获取设备状态成功", - "获取设备状态失败" - ), - search_weather: async (params) => await daily_wrap( - search_weather, - params, - "获取天气信息成功", - "获取天气信息失败" - ), - set_reminder: async (params) => await daily_wrap( - set_reminder, - params, - "设置提醒成功", - "设置提醒失败" - ), - schedule_one_time_task: async (params) => await daily_wrap( - schedule_one_time_task, - params, - "一次性定时任务创建成功", - "一次性定时任务创建失败" - ), - set_alarm: async (params) => await daily_wrap( - set_alarm, - params, - "设置闹钟成功", - "设置闹钟失败" - ), - send_message: async (params) => await daily_wrap( - send_message, - params, - "发送短信成功", - "发送短信失败" - ), - wechat_send_message: async (params) => await daily_wrap( - wechat_send_message, - params, - "调用微信发送消息成功", - "调用微信发送消息失败" - ), - qq_send_message: async (params) => await daily_wrap( - qq_send_message, - params, - "调用 QQ 发送消息成功", - "调用 QQ 发送消息失败" - ), - wechat_post_moments: async (params) => await daily_wrap( - wechat_post_moments, - params, - "调用微信朋友圈成功", - "调用微信朋友圈失败" - ), - make_phone_call: async (params) => await daily_wrap( - make_phone_call, - params, - "拨打电话成功", - "拨打电话失败" - ), - toggle_flashlight: async (params) => await daily_wrap( - toggle_flashlight, - params, - "手电筒操作成功", - "手电筒操作失败" - ), - adjust_volume: async (params) => await daily_wrap( - adjust_volume, - params, - "音量调节成功", - "音量调节失败" - ), - toggle_wifi: async (params) => await daily_wrap( - toggle_wifi, - params, - "Wi-Fi 操作成功", - "Wi-Fi 操作失败" - ), - take_screenshot: async (params) => await daily_wrap( - take_screenshot, - params, - "截图成功", - "截图失败" - ), - take_photo: async (params) => await daily_wrap( - take_photo, - params, - "打开相机成功", - "打开相机失败" - ), - toggle_dark_mode: async (params) => await daily_wrap( - toggle_dark_mode, - params, - "深夜模式操作成功", - "深夜模式操作失败" - ), - main: async (params) => await daily_wrap( - main, - params, - "主函数执行成功", - "主函数执行失败" - ) - } -})(); - -//逐个导出 -exports.get_current_date = dailyLife.get_current_date; -exports.device_status = dailyLife.device_status; -exports.search_weather = dailyLife.search_weather; -exports.set_reminder = dailyLife.set_reminder; -exports.schedule_one_time_task = dailyLife.schedule_one_time_task; -exports.set_alarm = dailyLife.set_alarm; -exports.send_message = dailyLife.send_message; -exports.wechat_send_message = dailyLife.wechat_send_message; -exports.qq_send_message = dailyLife.qq_send_message; -exports.wechat_post_moments = dailyLife.wechat_post_moments; -exports.make_phone_call = dailyLife.make_phone_call; -exports.toggle_flashlight = dailyLife.toggle_flashlight; -exports.adjust_volume = dailyLife.adjust_volume; -exports.main = dailyLife.main; -exports.toggle_wifi = dailyLife.toggle_wifi; -exports.take_screenshot = dailyLife.take_screenshot; -exports.take_photo = dailyLife.take_photo; -exports.toggle_dark_mode = dailyLife.toggle_dark_mode; diff --git a/scripts/douyin_download.ts b/scripts/douyin_download.ts deleted file mode 100644 index e869730..0000000 --- a/scripts/douyin_download.ts +++ /dev/null @@ -1,298 +0,0 @@ -/* -METADATA -{ - "name": "douyin_download", - - "display_name": { - "zh": "抖音下载工具", - "en": "Douyin Download Tool" - }, - "description": { "zh": "抖音工具包,提供从分享链接或分享口令中提取并下载无水印视频的功能。", "en": "Douyin toolkit for extracting and downloading watermark-free videos from share links or share codes." }, - "enabledByDefault": true, - "category": "Media", - "tools": [ - { - "name": "get_douyin_download_link", - "description": { "zh": "解析抖音分享链接或口令,下载无水印视频到本地", "en": "Parse a Douyin share link/code and download a watermark-free video to local storage." }, - "parameters": [ - { - "name": "input", - "description": { "zh": "抖音分享链接或包含链接的分享口令文本", "en": "Douyin share link, or share-code text that contains a link." }, - "type": "string", - "required": true - } - ] - }, - { - "name": "get_douyin_video_info", - "description": { "zh": "解析抖音分享链接或口令,仅获取视频信息和无水印下载链接,不下载视频", "en": "Parse a Douyin share link/code and return video info + watermark-free download URL (without downloading)." }, - "parameters": [ - { - "name": "input", - "description": { "zh": "抖音分享链接或包含链接的分享口令文本", "en": "Douyin share link, or share-code text that contains a link." }, - "type": "string", - "required": true - } - ] - } - ] -}*/ -const douyin = (function () { - /** - * 包装函数调用,提供标准化的成功/错误处理 - */ - async function douyin_wrap( - func: (params: T) => Promise, - params: T, - successMessage: string, - failMessage: string - ): Promise { - try { - console.log(`开始执行函数: ${func.name || '匿名函数'}`); - const result = await func(params); - complete({ success: true, message: successMessage, data: result }); - } catch (error: unknown) { - console.error(`函数 ${func.name || '匿名函数'} 执行失败! 错误: ${error instanceof Error ? error.message : String(error)}`); - complete({ success: false, message: `${failMessage}: ${error instanceof Error ? error.message : String(error)}`, error_stack: error instanceof Error ? error.stack : undefined }); - } - } - - const client = OkHttp.newClient(); - - // 抖音相关URL模式 - const DOUYIN_URL_PATTERNS = [ - /https?:\/\/v\.douyin\.com\/[^\s]+/, - /https?:\/\/www\.douyin\.com\/video\/[0-9]+/, - /https?:\/\/www\.douyin\.com\/share\/video\/[0-9]+/ - ]; - - /** - * 从文本中提取抖音链接 - */ - function extractDouyinUrl(text: string): string | null { - for (const pattern of DOUYIN_URL_PATTERNS) { - const match = text.match(pattern); - if (match) { - return match[0]; - } - } - return null; - } - - /** - * 解析抖音分享链接,获取视频信息 - */ - async function resolveDouyinUrl(shareUrl: string): Promise<{ videoId: string, downloadUrl: string, videoTitle: string }> { - try { - console.log(`正在解析抖音链接: ${shareUrl}`); - - const request = client.newRequest() - .url(shareUrl) - .method('GET') - .headers({ - "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) EdgiOS/121.0.2277.107 Version/17.0 Mobile/15E148 Safari/604.1", - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", - "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", - "Connection": "keep-alive" - }); - - const response = await request.build().execute(); - - if (!response.isSuccessful()) { - throw new Error(`链接解析失败 (${response.statusCode})`); - } - - const html = response.content; - - // 尝试从ROUTER_DATA中提取视频信息 - const routerDataMatch = html.match(/window\._ROUTER_DATA\s*=\s*({[\s\S]*?})<\/script>/); - if (routerDataMatch && routerDataMatch[1]) { - try { - const routerData = JSON.parse(routerDataMatch[1]); - // The actual key might vary, let's find it dynamically - const pageDataKey = Object.keys(routerData.loaderData).find(k => k.includes('/page')); - if (!pageDataKey) { - throw new Error("无法在 _ROUTER_DATA 中找到页面数据"); - } - const videoData = routerData.loaderData[pageDataKey]?.videoInfoRes?.item_list?.[0]; - - if (videoData) { - const videoId = videoData.aweme_id; - const videoTitle = videoData.desc; - const watermarkedUrl = videoData.video?.play_addr?.url_list?.[0]; - - if (videoId && videoTitle && watermarkedUrl) { - console.log(`通过 _ROUTER_DATA 找到视频信息: ID=${videoId}`); - const downloadUrl = watermarkedUrl.replace("playwm", "play"); - return { videoId, downloadUrl, videoTitle }; - } - } - } catch (e: unknown) { - console.error(`解析 _ROUTER_DATA JSON 失败: ${e instanceof Error ? e.message : String(e)}`); - } - } - - throw new Error("无法从HTML中解析出 _ROUTER_DATA"); - - } catch (error) { - console.error(`解析抖音链接时出错: ${error instanceof Error ? error.message : String(error)}`); - throw error; - } - } - - /** - * 获取抖音视频的无水印下载链接并下载视频 - */ - async function get_douyin_download_link(params: { input: string }): Promise { - const { input } = params; - - if (!input) { - throw new Error("输入内容不能为空"); - } - - console.log("开始获取并下载抖音无水印视频..."); - - try { - // 1. 提取和解析URL - const douyinUrl = extractDouyinUrl(input); - if (!douyinUrl) { - throw new Error("未找到有效的抖音链接"); - } - console.log(`找到抖音链接: ${douyinUrl}`); - - // 2. 解析链接并获取所有需要的视频信息 - console.log("正在解析分享链接..."); - const { videoId, downloadUrl, videoTitle: rawVideoTitle } = await resolveDouyinUrl(douyinUrl); - console.log(`解析到视频ID: ${videoId}`); - console.log(`获取到无水印下载链接: ${downloadUrl}`); - - // 获取视频标题(用于文件命名) - let videoTitle = (rawVideoTitle || `douyin_${videoId}`).trim(); - // 替换文件名中的非法字符 - videoTitle = videoTitle.replace(/[\\/:*?"<>|]/g, '_'); - - // 逐级创建目录并下载视频 - const baseDir = getPluginConfigDir("douyin_download"); - const destinationDir = `${baseDir}/downloads`; - const destinationPath = `${destinationDir}/${videoTitle}_${videoId}.mp4`; - - console.log(`确保目录存在: ${destinationDir}`); - // 逐级创建目录以确保路径存在 - const dirsToCreate = [baseDir, destinationDir]; - for (const dir of dirsToCreate) { - const mkdirResult = await Tools.Files.mkdir(dir); - if (!mkdirResult.successful) { - // 忽略"目录已存在"的错误,但记录其他可能的错误 - console.warn(`创建目录 '${dir}' 失败 (可能已存在): ${mkdirResult.details}`); - } - } - - console.log(`开始下载视频到: ${destinationPath}`); - const downloadResult = await Tools.Files.download(downloadUrl, destinationPath); - - if (!downloadResult.successful) { - throw new Error(`视频下载失败: ${downloadResult.details}`); - } - - const successMessage = `视频"${videoTitle}"成功下载到: ${destinationPath}`; - console.log(successMessage); - return successMessage; - - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - console.error(`处理失败: ${message}`); - // 重新抛出错误,以便包装器可以捕获它 - throw new Error(message); - } - } - - /** - * 获取抖音视频信息和无水印下载链接(不下载视频) - */ - async function get_douyin_video_info(params: { input: string }): Promise<{ videoId: string; videoTitle: string; downloadUrl: string; originalUrl: string }> { - const { input } = params; - - if (!input) { - throw new Error("输入内容不能为空"); - } - - console.log("开始解析抖音视频信息..."); - - try { - // 1. 提取URL - const douyinUrl = extractDouyinUrl(input); - if (!douyinUrl) { - throw new Error("未找到有效的抖音链接"); - } - console.log(`找到抖音链接: ${douyinUrl}`); - - // 2. 解析链接并获取视频信息 - console.log("正在解析分享链接..."); - const { videoId, downloadUrl, videoTitle } = await resolveDouyinUrl(douyinUrl); - console.log(`解析到视频ID: ${videoId}`); - console.log(`获取到无水印下载链接: ${downloadUrl}`); - console.log(`视频标题: ${videoTitle}`); - - const result = { - videoId, - videoTitle, - downloadUrl, - originalUrl: douyinUrl - }; - - console.log("视频信息解析成功"); - return result; - - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - console.error(`解析失败: ${message}`); - throw new Error(message); - } - } - - //region 测试函数 - /** - * 抖音工具功能测试主函数 - */ - async function main() { - console.log("🚀 开始执行抖音工具功能测试..."); - await test_download_link(); - const summary = `抖音下载工具测试完成!\n` + - `✅ 下载功能已测试`; - console.log(`\n${summary}`); - return summary; - } - - /** - * 测试下载功能 - */ - async function test_download_link() { - const testUrl = "3.00 05/24 Z@M.JI TLW:/ 这里有几首melodic dubstep,看你认识几个 # 电子音乐 # 音乐分享 # 顶级旋律 # 热门音乐🔥百听不厌 # 戴上耳机 https://v.douyin.com/AT8AfEbuP_k/ 复制此链接,打开Dou音搜索,直接观看视频!"; // 使用真实分享文本进行测试 - console.log(`1. 测试视频下载功能 (使用真实分享文本)`); - // 我们预期这个调用会成功提取URL并尝试下载 - try { - const result = await get_douyin_download_link({ input: testUrl }); - console.log(`✅ 下载功能测试成功, 结果: ${result}`); - return result; - } catch (e) { - const errorMessage = e instanceof Error ? e.message : String(e); - console.error(`❌ 下载功能测试失败: ${errorMessage}`); - // 在测试中,即使是预期的失败,也应该被视为一个需要注意的问题 - throw e; - } - } - //endregion - - return { - get_douyin_download_link: (params: { input: string; }) => douyin_wrap(get_douyin_download_link, params, '抖音视频下载完成', '抖音视频下载失败'), - get_douyin_video_info: (params: { input: string; }) => douyin_wrap(get_douyin_video_info, params, '抖音视频信息获取成功', '抖音视频信息获取失败'), - main: (params: Record) => douyin_wrap(main, params, '抖音工具测试完成', '抖音工具测试失败'), - test_download_link: (params: Record) => douyin_wrap(test_download_link, params, '下载链接获取测试成功', '下载链接获取测试失败'), - }; -})(); - -// 导出所有功能 -exports.get_douyin_download_link = douyin.get_douyin_download_link; -exports.get_douyin_video_info = douyin.get_douyin_video_info; -exports.main = douyin.main; -exports.test_download_link = douyin.test_download_link; diff --git a/scripts/duckduckgo.ts b/scripts/duckduckgo.ts deleted file mode 100644 index 8012f8f..0000000 --- a/scripts/duckduckgo.ts +++ /dev/null @@ -1,247 +0,0 @@ -/* -METADATA -{ - "name": "duckduckgo", - - "display_name": { - "zh": "DuckDuckGo 搜索", - "en": "DuckDuckGo Search" - }, - "description": { "zh": "使用DuckDuckGo进行网络搜索和内容抓取。", "en": "Use DuckDuckGo for web search and content extraction." }, - "category": "Search", - "tools": [ - { - "name": "search", - "description": { "zh": "执行DuckDuckGo搜索并返回格式化的结果。", "en": "Run a DuckDuckGo search and return formatted results." }, - "parameters": [ - { - "name": "query", - "description": { "zh": "搜索查询字符串", "en": "Search query string." }, - "type": "string", - "required": true - }, - { - "name": "max_results", - "description": { "zh": "返回的最大结果数 (默认: 10)", "en": "Maximum number of results to return (default: 10)." }, - "type": "string", - "required": false - } - ] - }, - { - "name": "fetch_content", - "description": { "zh": "从网页URL抓取和解析内容。", "en": "Fetch and parse content from a webpage URL." }, - "parameters": [ - { - "name": "url", - "description": { "zh": "要抓取内容的网页URL", "en": "Webpage URL to fetch and parse." }, - "type": "string", - "required": true - } - ] - } - ] -}*/ - -const duckduckgo = (function () { - const client = OkHttp.newClient(); - const BASE_URL = "https://html.duckduckgo.com/html/"; - - /** - * A simple rate limiter to avoid sending too many requests. - */ - class RateLimiter { - private requests: number[] = []; - constructor(private requestsPerMinute: number = 30) { } - - async acquire(): Promise { - const now = Date.now(); - // Remove requests older than 1 minute (60000 ms) - this.requests = this.requests.filter(reqTime => now - reqTime < 60000); - - if (this.requests.length >= this.requestsPerMinute) { - const timeSinceFirstRequest = now - this.requests[0]; - const waitTime = 60000 - timeSinceFirstRequest; - if (waitTime > 0) { - await new Promise(resolve => setTimeout(resolve, waitTime)); - } - } - - this.requests.push(Date.now()); - } - } - - const searchRateLimiter = new RateLimiter(30); - const fetchRateLimiter = new RateLimiter(20); - - /** - * Decodes basic HTML entities. - * @param text The text to decode. - * @returns Decoded text. - */ - function decodeHtmlEntities(text: string): string { - return text.replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/"/g, '"') - .replace(/'/g, "'"); - } - - /** - * Search DuckDuckGo and return formatted results. - * @param params Search parameters including query and max_results. - * @returns A formatted string of search results. - */ - async function search(params: { query: string; max_results?: string }): Promise { - const { query } = params; - let max_results = 10; - if (params.max_results) { - const parsedMaxResults = parseInt(params.max_results, 10); - if (!isNaN(parsedMaxResults)) { - max_results = parsedMaxResults; - } - } - - if (!query) { - throw new Error("查询不能为空"); - } - - await searchRateLimiter.acquire(); - console.log(`正在从DuckDuckGo搜索: ${query}`); - - const request = client.newRequest() - .url(BASE_URL) - .method('POST') - .body({ q: query, b: '', kl: '' }, 'form') - .headers({ - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" - }); - - const response = await request.build().execute(); - if (!response.isSuccessful()) { - throw new Error(`HTTP 错误! 状态码: ${response.statusCode}`); - } - - const html = response.content; - const results: { title: string; link: string; snippet: string; position: number }[] = []; - const resultRegex = /

[\s\S]*?([\s\S]+?)<\/a>[\s\S]*?<\/h2>[\s\S]*?([\s\S]+?)<\/a>/g; - - let match; - while ((match = resultRegex.exec(html)) !== undefined) { - if (results.length >= max_results) { - break; - } - - let link = match[1]; - if (link.includes('y.js')) { - continue; // Skip ads - } - - if (link.startsWith('//duckduckgo.com/l/?uddg=')) { - try { - link = decodeURIComponent(link.split('uddg=')[1].split('&')[0]); - } catch (e) { - console.error(`URL 解码失败: ${link}`); - } - } - - const title = decodeHtmlEntities(match[2].replace(/<[^>]*>/g, '').trim()); - const snippet = decodeHtmlEntities(match[3].replace(/<[^>]*>/g, '').trim()); - - results.push({ title, link, snippet, position: results.length + 1 }); - } - - console.log(`成功找到 ${results.length} 个结果`); - return format_results_for_llm(results); - } - - /** - * Fetch and parse content from a webpage URL. - * @param params Parameters including the URL to fetch. - * @returns The cleaned text content of the webpage. - */ - async function fetch_content(params: { url: string }): Promise { - const { url } = params; - if (!url) { - throw new Error("URL不能为空"); - } - - await fetchRateLimiter.acquire(); - console.log(`正在抓取内容: ${url}`); - - try { - const request = client.newRequest() - .url(url) - .method('GET') - .headers({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" }); - - const response = await request.build().execute(); - if (!response.isSuccessful()) { - throw new Error(`无法访问网页 (${response.statusCode})`); - } - - let text = response.content; - text = text.replace(/]*>[\s\S]*?<\/style>/gi, ''); - text = text.replace(/]*>[\s\S]*?<\/script>/gi, ''); - text = text.replace(/]*>[\s\S]*?<\/nav>/gi, ''); - text = text.replace(/]*>[\s\S]*?<\/header>/gi, ''); - text = text.replace(/]*>[\s\S]*?<\/footer>/gi, ''); - text = text.replace(/<[^>]+>/g, ' '); - text = text.replace(/\s+/g, ' ').trim(); - - if (text.length > 8000) { - text = text.substring(0, 8000) + "... [内容被截断]"; - } - - console.log(`成功抓取并解析内容 (${text.length} 字符)`); - return text; - } catch (error) { - console.error(`从 ${url} 抓取内容时出错: ${error.message}`); - return `错误: 从网页抓取内容时发生意外错误 (${error.message})`; - } - } - - /** - * Formats search results into a readable string for LLMs. - * @param results The list of search results. - * @returns A formatted string. - */ - function format_results_for_llm(results: { title: string; link: string; snippet: string; position: number }[]): string { - if (!results || results.length === 0) { - return "没有为您的搜索查询找到结果。这可能是由于DuckDuckGo的机器人检测或查询没有匹配项。请尝试重新措辞您的搜索或在几分钟后重试。"; - } - - const output = results.map(r => - `${r.position}. ${r.title}\n URL: ${r.link}\n 摘要: ${r.snippet}` - ); - - return `找到 ${results.length} 个搜索结果:\n\n${output.join('\n\n')}`; - } - - /** - * Wraps function calls for standardized success/error handling. - */ - async function duckduckgo_wrap( - func: (params: any) => Promise, - params: any, - successMessage: string, - failMessage: string - ): Promise { - try { - console.log(`开始执行函数: ${func.name || '匿名函数'}`); - const result = await func(params); - complete({ success: true, message: successMessage, data: result }); - } catch (error) { - console.error(`函数 ${func.name || '匿名函数'} 执行失败! 错误: ${error.message}`); - complete({ success: false, message: `${failMessage}: ${error.message}`, error_stack: error.stack }); - } - } - - return { - search: (params: any) => duckduckgo_wrap(search, params, '搜索完成', '搜索失败'), - fetch_content: (params: any) => duckduckgo_wrap(fetch_content, params, '内容抓取完成', '内容抓取失败'), - }; -})(); - -exports.search = duckduckgo.search; -exports.fetch_content = duckduckgo.fetch_content; diff --git a/scripts/extended_chat.ts b/scripts/extended_chat.ts deleted file mode 100644 index 17ee566..0000000 --- a/scripts/extended_chat.ts +++ /dev/null @@ -1,569 +0,0 @@ -/* METADATA -{ - "name": "extended_chat", - - "display_name": { - "zh": "增强对话", - "en": "Extended Chat" - }, - "description": { - "zh": "对话工具包:列出/查找/重命名/删除对话、跨话题读取消息、绑定角色卡对话并发送消息。", - "en": "Chat toolkit: list/find/rename/delete chats, read messages across chats, bind character cards and send messages." - }, - "enabledByDefault": true, - "category": "Chat", - "tools": [ - { - "name": "list_chats", - "description": { - "zh": "列出并筛选对话(用于获取 chat_id)。", - "en": "List and filter chats (to discover chat_id)." - }, - "parameters": [ - { "name": "query", "description": { "zh": "可选:标题筛选关键字", "en": "Optional title keyword" }, "type": "string", "required": false }, - { "name": "match", "description": { "zh": "可选:contains/exact/regex(默认 contains)", "en": "Optional: contains/exact/regex (default contains)" }, "type": "string", "required": false }, - { "name": "limit", "description": { "zh": "可选:最多返回条数(默认 50)", "en": "Optional max results (default 50)" }, "type": "number", "required": false }, - { "name": "sort_by", "description": { "zh": "可选:updatedAt/createdAt/messageCount(默认 updatedAt)", "en": "Optional: updatedAt/createdAt/messageCount (default updatedAt)" }, "type": "string", "required": false }, - { "name": "sort_order", "description": { "zh": "可选:asc/desc(默认 desc)", "en": "Optional: asc/desc (default desc)" }, "type": "string", "required": false } - ] - }, - { - "name": "find_chat", - "description": { - "zh": "按标题查找一个对话并返回 chat_id。", - "en": "Find a single chat by title and return chat_id." - }, - "parameters": [ - { "name": "query", "description": { "zh": "标题关键字/正则", "en": "Title keyword/regex" }, "type": "string", "required": true }, - { "name": "match", "description": { "zh": "可选:contains/exact/regex(默认 contains)", "en": "Optional: contains/exact/regex (default contains)" }, "type": "string", "required": false }, - { "name": "index", "description": { "zh": "可选:当匹配多个时选择第 N 个(默认 0)", "en": "Optional: pick Nth when multiple matches (default 0)" }, "type": "number", "required": false } - ] - }, - { - "name": "read_messages", - "description": { - "zh": "读取指定对话的消息(可按 chat_id 或 chat_title 指定)。", - "en": "Read messages from a chat (by chat_id or chat_title)." - }, - "parameters": [ - { "name": "chat_id", "description": { "zh": "目标对话 ID(可选)", "en": "Target chat id (optional)" }, "type": "string", "required": false }, - { "name": "chat_title", "description": { "zh": "目标对话标题(可选;当 chat_id 为空时使用)", "en": "Target chat title (optional; used when chat_id is empty)" }, "type": "string", "required": false }, - { "name": "chat_query", "description": { "zh": "可选:标题筛选关键字(当 chat_id/chat_title 为空时使用)", "en": "Optional title keyword (used when chat_id/chat_title is empty)" }, "type": "string", "required": false }, - { "name": "chat_index", "description": { "zh": "可选:当筛选结果有多个时选择第 N 个(默认 0)", "en": "Optional: pick Nth when multiple matches (default 0)" }, "type": "number", "required": false }, - { "name": "match", "description": { "zh": "可选:contains/exact/regex(默认 contains)", "en": "Optional: contains/exact/regex (default contains)" }, "type": "string", "required": false }, - { "name": "order", "description": { "zh": "可选:asc/desc(默认 desc)", "en": "Optional: asc/desc (default desc)" }, "type": "string", "required": false }, - { "name": "limit", "description": { "zh": "可选:返回消息条数(默认 20)", "en": "Optional: max number of messages (default 20)" }, "type": "number", "required": false } - ] - }, - { - "name": "rename_chat", - "description": { - "zh": "重命名指定对话(可按 chat_id 或 chat_title 指定)。", - "en": "Rename a chat (by chat_id or chat_title)." - }, - "parameters": [ - { "name": "new_title", "description": { "zh": "新的对话标题", "en": "New chat title" }, "type": "string", "required": true }, - { "name": "chat_id", "description": { "zh": "目标对话 ID(可选)", "en": "Target chat id (optional)" }, "type": "string", "required": false }, - { "name": "chat_title", "description": { "zh": "目标对话标题(可选;当 chat_id 为空时使用)", "en": "Target chat title (optional; used when chat_id is empty)" }, "type": "string", "required": false }, - { "name": "chat_query", "description": { "zh": "可选:标题筛选关键字(当 chat_id/chat_title 为空时使用)", "en": "Optional title keyword (used when chat_id/chat_title is empty)" }, "type": "string", "required": false }, - { "name": "chat_index", "description": { "zh": "可选:当筛选结果有多个时选择第 N 个(默认 0)", "en": "Optional: pick Nth when multiple matches (default 0)" }, "type": "number", "required": false }, - { "name": "match", "description": { "zh": "可选:contains/exact/regex(默认 contains)", "en": "Optional: contains/exact/regex (default contains)" }, "type": "string", "required": false } - ] - }, - { - "name": "delete_chat", - "description": { - "zh": "删除指定对话(可按 chat_id 或 chat_title 指定)。", - "en": "Delete a chat (by chat_id or chat_title)." - }, - "parameters": [ - { "name": "chat_id", "description": { "zh": "目标对话 ID(可选)", "en": "Target chat id (optional)" }, "type": "string", "required": false }, - { "name": "chat_title", "description": { "zh": "目标对话标题(可选;当 chat_id 为空时使用)", "en": "Target chat title (optional; used when chat_id is empty)" }, "type": "string", "required": false }, - { "name": "chat_query", "description": { "zh": "可选:标题筛选关键字(当 chat_id/chat_title 为空时使用)", "en": "Optional title keyword (used when chat_id/chat_title is empty)" }, "type": "string", "required": false }, - { "name": "chat_index", "description": { "zh": "可选:当筛选结果有多个时选择第 N 个(默认 0)", "en": "Optional: pick Nth when multiple matches (default 0)" }, "type": "number", "required": false }, - { "name": "match", "description": { "zh": "可选:contains/exact/regex(默认 contains)", "en": "Optional: contains/exact/regex (default contains)" }, "type": "string", "required": false } - ] - }, - { - "name": "chat_with_agent", - "description": { - "zh": "与对应角色的 agent 对话:传入角色卡名称;chat_id 为空时自动创建新对话并返回新 ID。严格执行一角色一会话,不能多个角色共用同一会话。该工具可用于将任务分担给其他 agent 或与其他角色交流,但通常只有在用户明确表达此意图时使用;多数情况下,能自行完成的任务应优先直接完成。", - "en": "Chat with the agent for the specified character card name; if chat_id is empty, create a new chat and return its ID. Enforces one role per chat (no sharing between roles). Use this tool to delegate tasks to other agents or communicate with other roles when the user explicitly intends it; otherwise, prefer completing tasks directly without using this tool." - }, - "parameters": [ - { "name": "message", "description": { "zh": "发送给 AI 的内容", "en": "Message to send to AI" }, "type": "string", "required": true }, - { "name": "character_card_name", "description": { "zh": "角色卡名称", "en": "Character card name" }, "type": "string", "required": true }, - { "name": "chat_id", "description": { "zh": "目标对话 ID(可选;为空时新建)", "en": "Target chat id (optional; create new if empty)" }, "type": "string", "required": false }, - { "name": "timeout", "description": { "zh": "可选:等待返回的超时秒数(默认 180)", "en": "Optional timeout seconds to wait for response (default 180)" }, "type": "number", "required": false }, - { "name": "persist_turn", "description": { "zh": "可选:是否持久化本轮用户消息和 AI 回复(默认 true)", "en": "Optional: whether to persist this turn's user message and AI reply (default true)" }, "type": "boolean", "required": false }, - { "name": "notify_reply", "description": { "zh": "可选:是否覆盖本轮回复通知开关", "en": "Optional: override reply notification for this turn" }, "type": "boolean", "required": false }, - { "name": "hide_user_message", "description": { "zh": "可选:是否在 UI 中隐藏用户消息正文并显示占位标记", "en": "Optional: hide the user message body in UI and show a placeholder marker" }, "type": "boolean", "required": false }, - { "name": "disable_warning", "description": { "zh": "可选:是否关闭本轮 AI 生成的 warning 标记", "en": "Optional: suppress AI-generated warning markup for this turn" }, "type": "boolean", "required": false } - ] - }, - { - "name": "agent_status", - "description": { - "zh": "查询对话的输入处理状态。", - "en": "Check a chat's input processing status." - }, - "parameters": [ - { "name": "chat_id", "description": { "zh": "目标对话 ID", "en": "Target chat id" }, "type": "string", "required": true } - ] - }, - { - "name": "list_character_cards", - "description": { - "zh": "列出所有角色卡(用于获取 character_card_id)。", - "en": "List all character cards (to discover character_card_id)." - }, - "parameters": [] - } - ] -}*/ - -const HistoryChat = (function () { - - interface ToolResponse { - success: boolean; - message: string; - data?: T; - } - - type ResolveChatIdParams = { - chat_id?: string; - chat_title?: string; - chat_query?: string; - chat_index?: number; - match?: string; - }; - - type ReadMessagesParams = ResolveChatIdParams & { - order?: string; - limit?: number; - }; - - type RenameChatParams = ResolveChatIdParams & { - new_title: string; - }; - - type DeleteChatParams = ResolveChatIdParams; - - type ListChatsParams = { - query?: string; - match?: string; - limit?: number; - sort_by?: string; - sort_order?: string; - }; - - type FindChatParams = { - query: string; - match?: string; - index?: number; - }; - - type AgentStatusParams = { - chat_id: string; - }; - - type ChatWithAgentParams = { - message: string; - character_card_name: string; - chat_id?: string; - timeout?: number; - persist_turn?: boolean; - notify_reply?: boolean; - hide_user_message?: boolean; - disable_warning?: boolean; - }; - - function normalizeMatchMode(match?: string): 'contains' | 'exact' | 'regex' { - const m = (match || '').trim().toLowerCase(); - if (m === 'exact' || m === 'regex' || m === 'contains') return m; - return 'contains'; - } - - async function list_chats_impl(params: ListChatsParams): Promise { - const query = (params?.query ?? '').toString().trim(); - const matchMode = normalizeMatchMode(params?.match); - const limitRaw = params && params.limit !== undefined ? Number(params.limit) : undefined; - const limit = limitRaw !== undefined && !isNaN(limitRaw) ? limitRaw : undefined; - const sortBy = params?.sort_by ? params.sort_by.toString().trim() : undefined; - const sortOrder = params?.sort_order ? params.sort_order.toString().trim().toLowerCase() : undefined; - - const listParams: ToolParams = {}; - if (query) listParams.query = query; - if (matchMode) listParams.match = matchMode; - if (limit !== undefined) listParams.limit = limit; - if (sortBy) listParams.sort_by = sortBy; - if (sortOrder) listParams.sort_order = sortOrder; - - const listResult = await Tools.Chat.listChats(listParams); - const chats = listResult.chats; - return { - success: true, - message: '对话列表获取完成', - data: { - totalCount: listResult?.totalCount ?? chats.length, - currentChatId: listResult?.currentChatId ?? null, - matchedCount: listResult?.totalCount ?? chats.length, - chats, - } - }; - } - - async function find_chat_impl(params: FindChatParams): Promise { - const query = (params?.query ?? '').toString().trim(); - if (!query) { - throw new Error('Missing parameter: query'); - } - - const matchMode = normalizeMatchMode(params?.match); - const indexRaw = params && params.index !== undefined ? Number(params.index) : 0; - const index = isNaN(indexRaw) ? 0 : indexRaw; - const findParams: Parameters[0] = { query }; - if (matchMode) findParams.match = matchMode; - if (index !== undefined) findParams.index = index; - const findResult = await Tools.Chat.findChat(findParams); - const picked = findResult?.chat ?? null; - if (!picked) { - throw new Error(`Chat not found by query: ${query}`); - } - - return { - success: true, - message: '对话查找完成', - data: { - chat: picked, - matchedCount: findResult?.matchedCount ?? 1, - } - }; - } - - async function resolveChatId(params: ResolveChatIdParams): Promise { - if (params && typeof params.chat_id === 'string' && params.chat_id.trim()) { - return params.chat_id.trim(); - } - - const title = params && typeof params.chat_title === 'string' ? params.chat_title.trim() : ''; - const query = params && typeof params.chat_query === 'string' ? params.chat_query.trim() : ''; - const matchMode = normalizeMatchMode(params?.match); - const indexRaw = params && params.chat_index !== undefined ? Number(params.chat_index) : 0; - const index = isNaN(indexRaw) ? 0 : indexRaw; - - if (!title && !query) { - throw new Error('Missing parameter: chat_id or chat_title or chat_query is required'); - } - - const needle = title || query; - const findParams: Parameters[0] = { query: needle }; - findParams.match = title ? 'exact' : matchMode; - if (index !== undefined) findParams.index = index; - const findResult = await Tools.Chat.findChat(findParams); - const picked = findResult?.chat ?? null; - if (!picked?.id) { - throw new Error(`Chat not found by query: ${needle}`); - } - return picked.id; - } - - async function read_messages_impl(params: ReadMessagesParams): Promise { - const chatId = await resolveChatId(params || {}); - - const orderRaw = params && params.order !== undefined ? String(params.order).trim().toLowerCase() : ''; - const order = (orderRaw === 'asc' || orderRaw === 'desc') ? orderRaw : 'desc'; - - const limitRaw = params && params.limit !== undefined ? Number(params.limit) : 20; - const limit = isNaN(limitRaw) ? 20 : limitRaw; - - const result = await Tools.Chat.getMessages(chatId, { - order, - limit, - }); - - const rawMessages = result.messages; - const text = rawMessages - .map((m) => { - const role = (m.roleName ?? m.sender ?? '').toString() || 'message'; - const ts = (m.timestamp !== undefined && m.timestamp !== null) ? String(m.timestamp) : ''; - const header = ts ? `[${ts}] ${role}` : role; - return `${header}:\n${(m.content ?? '').toString()}`; - }) - .join('\n\n'); - - return { - success: true, - message: '读取对话消息完成', - data: { - result, - text, - }, - }; - } - - async function rename_chat_impl(params: RenameChatParams): Promise { - const newTitle = (params?.new_title ?? '').toString().trim(); - if (!newTitle) { - throw new Error('Missing parameter: new_title'); - } - - const chatId = await resolveChatId(params || {}); - const result = await Tools.Chat.updateTitle(chatId, newTitle); - - return { - success: true, - message: '对话重命名完成', - data: { - chat_id: chatId, - title: newTitle, - result, - }, - }; - } - - async function delete_chat_impl(params: DeleteChatParams): Promise { - const chatId = await resolveChatId(params || {}); - const result = await Tools.Chat.deleteChat(chatId); - - return { - success: true, - message: '对话删除完成', - data: { - chat_id: chatId, - result, - }, - }; - } - - async function agent_status_impl(params: AgentStatusParams): Promise { - const chatId = (params?.chat_id ?? '').toString().trim(); - if (!chatId) { - throw new Error('Missing parameter: chat_id'); - } - const result = await Tools.Chat.agentStatus(chatId); - return { - success: true, - message: '对话状态查询完成', - data: { - result, - }, - }; - } - - async function list_character_cards_impl(): Promise { - const result = await Tools.Chat.listCharacterCards(); - const cards = result.cards; - return { - success: true, - message: '角色卡列表获取完成', - data: { - totalCount: result?.totalCount ?? cards.length, - cards, - }, - }; - } - - async function chat_with_agent_impl(params: ChatWithAgentParams): Promise { - const message = (params?.message ?? '').toString(); - const characterCardNameInput = (params?.character_card_name ?? '').toString().trim(); - if (!message.trim()) { - throw new Error('Missing parameter: message'); - } - if (!characterCardNameInput) { - throw new Error('Missing parameter: character_card_name'); - } - - let characterCardName = characterCardNameInput; - let characterCardId = ''; - try { - const cardResult = await Tools.Chat.listCharacterCards(); - const cards = cardResult.cards; - const targetCard = cards.find((card) => card.name === characterCardNameInput); - if (!targetCard) { - throw new Error(`Character card not found: ${characterCardNameInput}`); - } - characterCardName = targetCard.name; - characterCardId = targetCard.id; - } catch { - if (!characterCardId) { - throw new Error(`Character card not found: ${characterCardNameInput}`); - } - } - - try { - await Tools.Chat.startService(); - } catch { - // ignore service start errors to avoid blocking agent message - } - - let chatId = (params?.chat_id ?? '').toString().trim(); - if (!chatId) { - const lang = (getLang() || '').toLowerCase(); - const group = lang === 'zh' ? '子任务' : 'subTask'; - const creation = await Tools.Chat.createNew( - group, - false, - characterCardId, - ); - chatId = (creation?.chatId ?? '').toString().trim(); - if (!chatId) { - throw new Error('Failed to create new chat'); - } - } else { - const findResult = await Tools.Chat.findChat({ - query: chatId, - match: 'exact', - index: 0, - }); - const boundName = findResult?.chat?.characterCardName ?? null; - if (boundName && boundName !== characterCardName) { - throw new Error(`Chat ${chatId} 已绑定角色 ${boundName},不能与 ${characterCardName} 共用会话`); - } - } - - const timeoutRaw = params?.timeout !== undefined ? Number(params.timeout) : 180; - const timeoutSec = isNaN(timeoutRaw) || timeoutRaw <= 0 ? 180 : timeoutRaw; - const timeoutMs = timeoutSec * 1000; - - const sendMessageOptions: Parameters[4] = {}; - if (params?.persist_turn !== undefined) { - sendMessageOptions.persist_turn = params.persist_turn; - } - if (params?.notify_reply !== undefined) { - sendMessageOptions.notify_reply = params.notify_reply; - } - if (params?.hide_user_message !== undefined) { - sendMessageOptions.hide_user_message = params.hide_user_message; - } - if (params?.disable_warning !== undefined) { - sendMessageOptions.disable_warning = params.disable_warning; - } - sendMessageOptions.timeout_ms = timeoutMs; - const sendPromise = Tools.Chat.sendMessage( - message, - chatId, - characterCardId, - getCallerName() || characterCardName, - sendMessageOptions, - ); - - const timeoutPromise = new Promise((resolve) => { - setTimeout(() => resolve(null), timeoutMs); - }); - - const sendResult = await Promise.race([sendPromise, timeoutPromise]); - if (sendResult === null) { - return { - success: true, - message: `已发送给 ${characterCardName},等待响应超时(${timeoutSec}s)`, - data: { - chat_id: chatId, - timeout: true, - hint: '可以通过 agent_status 查看该 agent 是否已处理你的问题。', - }, - }; - } - - return { - success: true, - message: `发消息给 ${characterCardName}`, - data: { - chat_id: chatId, - result: sendResult, - }, - }; - } - - async function wrapToolExecution

(func: (params: P) => Promise, params: P): Promise { - try { - const result = await func(params); - complete(result); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error); - console.error(`Tool ${func.name} failed unexpectedly`, error); - complete({ - success: false, - message: `读取对话消息失败: ${message}`, - }); - } - } - - async function wrapToolExecutionNoParams(func: () => Promise): Promise { - try { - const result = await func(); - complete(result); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error); - console.error(`Tool ${func.name} failed unexpectedly`, error); - complete({ - success: false, - message: `读取对话消息失败: ${message}`, - }); - } - } - - async function read_messages(params: ReadMessagesParams): Promise { - return await wrapToolExecution(read_messages_impl, params); - } - - async function rename_chat(params: RenameChatParams): Promise { - return await wrapToolExecution(rename_chat_impl, params); - } - - async function delete_chat(params: DeleteChatParams): Promise { - return await wrapToolExecution(delete_chat_impl, params); - } - - async function list_chats(params: ListChatsParams): Promise { - return await wrapToolExecution(list_chats_impl, params); - } - - async function find_chat(params: FindChatParams): Promise { - return await wrapToolExecution(find_chat_impl, params); - } - - async function agent_status(params: AgentStatusParams): Promise { - return await wrapToolExecution(agent_status_impl, params); - } - - async function list_character_cards(): Promise { - return await wrapToolExecutionNoParams(list_character_cards_impl); - } - - async function chat_with_agent(params: ChatWithAgentParams): Promise { - return await wrapToolExecution(chat_with_agent_impl, params); - } - - async function main(): Promise { - complete({ - success: true, - message: 'extended_chat 工具包已加载', - data: { - hint: 'Use extended_chat:read_messages / rename_chat / delete_chat.', - }, - }); - } - - return { - list_chats, - find_chat, - read_messages, - rename_chat, - delete_chat, - chat_with_agent, - agent_status, - list_character_cards, - main, - }; -})(); - -exports.list_chats = HistoryChat.list_chats; -exports.find_chat = HistoryChat.find_chat; -exports.read_messages = HistoryChat.read_messages; -exports.rename_chat = HistoryChat.rename_chat; -exports.delete_chat = HistoryChat.delete_chat; -exports.chat_with_agent = HistoryChat.chat_with_agent; -exports.agent_status = HistoryChat.agent_status; -exports.list_character_cards = HistoryChat.list_character_cards; -exports.main = HistoryChat.main; diff --git a/scripts/extended_file_tools.ts b/scripts/extended_file_tools.ts deleted file mode 100644 index 5b04bc2..0000000 --- a/scripts/extended_file_tools.ts +++ /dev/null @@ -1,198 +0,0 @@ -/* METADATA -{ - "name": "extended_file_tools", - - "display_name": { - "zh": "增强文件工具", - "en": "Extended File Tools" - }, - "description": { - "zh": "拓展文件工具包:提供 file_exists / move_file / copy_file / file_info / unzip_files / zip_files / open_file / share_file(默认文件工具中已移除这些项)。", - "en": "Extended file tools: file_exists / move_file / copy_file / file_info / unzip_files / zip_files / open_file / share_file (removed from default file tools)." - }, - "category": "File", - "enabledByDefault": true, - "tools": [ - { - "name": "file_exists", - "description": { "zh": "检查文件或目录是否存在。", "en": "Check if a file or directory exists." }, - "parameters": [ - { "name": "path", "description": { "zh": "目标路径", "en": "Target path" }, "type": "string", "required": true }, - { "name": "environment", "description": { "zh": "可选:android/linux", "en": "Optional: android/linux" }, "type": "string", "required": false } - ] - }, - { - "name": "move_file", - "description": { "zh": "移动或重命名文件/目录。", "en": "Move or rename a file/directory." }, - "parameters": [ - { "name": "source", "description": { "zh": "源路径", "en": "Source path" }, "type": "string", "required": true }, - { "name": "destination", "description": { "zh": "目标路径", "en": "Destination path" }, "type": "string", "required": true }, - { "name": "environment", "description": { "zh": "可选:android/linux", "en": "Optional: android/linux" }, "type": "string", "required": false } - ] - }, - { - "name": "copy_file", - "description": { "zh": "复制文件/目录(支持跨环境复制)。", "en": "Copy a file/directory (supports cross-environment copy)." }, - "parameters": [ - { "name": "source", "description": { "zh": "源路径", "en": "Source path" }, "type": "string", "required": true }, - { "name": "destination", "description": { "zh": "目标路径", "en": "Destination path" }, "type": "string", "required": true }, - { "name": "recursive", "description": { "zh": "可选:是否递归,默认 false", "en": "Optional: recursive (default: false)" }, "type": "boolean", "required": false }, - { "name": "source_environment", "description": { "zh": "可选:源环境 android/linux", "en": "Optional: source environment android/linux" }, "type": "string", "required": false }, - { "name": "dest_environment", "description": { "zh": "可选:目标环境 android/linux", "en": "Optional: destination environment android/linux" }, "type": "string", "required": false } - ] - }, - { - "name": "file_info", - "description": { "zh": "获取文件/目录信息。", "en": "Get file/directory info." }, - "parameters": [ - { "name": "path", "description": { "zh": "目标路径", "en": "Target path" }, "type": "string", "required": true }, - { "name": "environment", "description": { "zh": "可选:android/linux", "en": "Optional: android/linux" }, "type": "string", "required": false } - ] - }, - { - "name": "zip_files", - "description": { "zh": "压缩文件/目录。", "en": "Zip files/directories." }, - "parameters": [ - { "name": "source", "description": { "zh": "源路径", "en": "Source path" }, "type": "string", "required": true }, - { "name": "destination", "description": { "zh": "输出 zip 文件路径", "en": "Destination zip path" }, "type": "string", "required": true }, - { "name": "environment", "description": { "zh": "可选:android/linux", "en": "Optional: android/linux" }, "type": "string", "required": false }, - { "name": "include_root_directory", "description": { "zh": "压缩目录时是否保留源目录名作为顶层目录,默认 true", "en": "When zipping a directory, keep the source directory name as the top-level folder, default true" }, "type": "boolean", "required": false } - ] - }, - { - "name": "unzip_files", - "description": { "zh": "解压 zip 文件。", "en": "Unzip an archive." }, - "parameters": [ - { "name": "source", "description": { "zh": "zip 文件路径", "en": "Zip file path" }, "type": "string", "required": true }, - { "name": "destination", "description": { "zh": "解压目录", "en": "Destination directory" }, "type": "string", "required": true }, - { "name": "environment", "description": { "zh": "可选:android/linux", "en": "Optional: android/linux" }, "type": "string", "required": false } - ] - }, - { - "name": "open_file", - "description": { "zh": "用系统默认应用打开文件。", "en": "Open a file with system default app." }, - "parameters": [ - { "name": "path", "description": { "zh": "文件路径", "en": "File path" }, "type": "string", "required": true }, - { "name": "environment", "description": { "zh": "可选:android/linux", "en": "Optional: android/linux" }, "type": "string", "required": false } - ] - }, - { - "name": "share_file", - "description": { "zh": "分享文件给其他应用。", "en": "Share a file with other apps." }, - "parameters": [ - { "name": "path", "description": { "zh": "文件路径", "en": "File path" }, "type": "string", "required": true }, - { "name": "title", "description": { "zh": "可选:分享标题", "en": "Optional: share title" }, "type": "string", "required": false }, - { "name": "environment", "description": { "zh": "可选:android/linux", "en": "Optional: android/linux" }, "type": "string", "required": false } - ] - } - ] -}*/ - -const ExtendedFileTools = (function () { - - interface ToolResponse { - success: boolean; - message: string; - data?: any; - } - - type FileEnvironment = 'android' | 'linux'; - - async function file_exists(params: { path: string; environment?: FileEnvironment }): Promise { - const result = await Tools.Files.exists(params.path, params.environment); - return { success: !!result && (result.exists ?? true), message: '检查完成', data: result }; - } - - async function move_file(params: { source: string; destination: string; environment?: FileEnvironment }): Promise { - const result = await Tools.Files.move(params.source, params.destination, params.environment); - return { success: !!result, message: '移动完成', data: result }; - } - - async function copy_file(params: { source: string; destination: string; recursive?: boolean; source_environment?: FileEnvironment; dest_environment?: FileEnvironment }): Promise { - const result = await Tools.Files.copy(params.source, params.destination, params.recursive, params.source_environment, params.dest_environment); - return { success: !!result, message: '复制完成', data: result }; - } - - async function file_info(params: { path: string; environment?: FileEnvironment }): Promise { - const result = await Tools.Files.info(params.path, params.environment); - return { success: !!result, message: '获取信息完成', data: result }; - } - - async function zip_files(params: { source: string; destination: string; environment?: FileEnvironment; include_root_directory?: boolean }): Promise { - const result = await Tools.Files.zip( - params.source, - params.destination, - params.environment, - params.include_root_directory - ); - return { success: !!result, message: '压缩完成', data: result }; - } - - async function unzip_files(params: { source: string; destination: string; environment?: FileEnvironment }): Promise { - const result = await Tools.Files.unzip(params.source, params.destination, params.environment); - return { success: !!result, message: '解压完成', data: result }; - } - - async function open_file(params: { path: string; environment?: FileEnvironment }): Promise { - const result = await Tools.Files.open(params.path, params.environment); - return { success: !!result, message: '打开文件完成', data: result }; - } - - async function share_file(params: { path: string; title?: string; environment?: FileEnvironment }): Promise { - const result = await Tools.Files.share(params.path, params.title, params.environment); - return { success: !!result, message: '分享文件完成', data: result }; - } - - async function wrapToolExecution

(func: (params: P) => Promise, params: P) { - try { - const result = await func(params); - complete(result); - } catch (error: any) { - console.error(`Tool ${func.name} failed unexpectedly`, error); - complete({ - success: false, - message: `工具执行时发生意外错误: ${error.message}`, - }); - } - } - - async function main() { - const results: any[] = []; - results.push({ tool: 'file_exists', result: { success: null, message: '未测试' } }); - results.push({ tool: 'move_file', result: { success: null, message: '未测试(会移动/重命名文件)' } }); - results.push({ tool: 'copy_file', result: { success: null, message: '未测试(会复制文件)' } }); - results.push({ tool: 'file_info', result: { success: null, message: '未测试' } }); - results.push({ tool: 'zip_files', result: { success: null, message: '未测试(会写入zip文件)' } }); - results.push({ tool: 'unzip_files', result: { success: null, message: '未测试(会写入解压文件)' } }); - results.push({ tool: 'open_file', result: { success: null, message: '未测试(会拉起系统应用)' } }); - results.push({ tool: 'share_file', result: { success: null, message: '未测试(会弹出分享面板)' } }); - - complete({ - success: true, - message: '拓展文件工具包加载完成(未执行破坏性测试)', - data: { results } - }); - } - - return { - file_exists: (params: { path: string; environment?: FileEnvironment }) => wrapToolExecution(file_exists, params), - move_file: (params: { source: string; destination: string; environment?: FileEnvironment }) => wrapToolExecution(move_file, params), - copy_file: (params: { source: string; destination: string; recursive?: boolean; source_environment?: FileEnvironment; dest_environment?: FileEnvironment }) => wrapToolExecution(copy_file, params), - file_info: (params: { path: string; environment?: FileEnvironment }) => wrapToolExecution(file_info, params), - zip_files: (params: { source: string; destination: string; environment?: FileEnvironment; include_root_directory?: boolean }) => wrapToolExecution(zip_files, params), - unzip_files: (params: { source: string; destination: string; environment?: FileEnvironment }) => wrapToolExecution(unzip_files, params), - open_file: (params: { path: string; environment?: FileEnvironment }) => wrapToolExecution(open_file, params), - share_file: (params: { path: string; title?: string; environment?: FileEnvironment }) => wrapToolExecution(share_file, params), - main, - }; -})(); - -exports.file_exists = ExtendedFileTools.file_exists; -exports.move_file = ExtendedFileTools.move_file; -exports.copy_file = ExtendedFileTools.copy_file; -exports.file_info = ExtendedFileTools.file_info; -exports.zip_files = ExtendedFileTools.zip_files; -exports.unzip_files = ExtendedFileTools.unzip_files; -exports.open_file = ExtendedFileTools.open_file; -exports.share_file = ExtendedFileTools.share_file; -exports.main = ExtendedFileTools.main; diff --git a/scripts/extended_http_tools.ts b/scripts/extended_http_tools.ts deleted file mode 100644 index 9449eba..0000000 --- a/scripts/extended_http_tools.ts +++ /dev/null @@ -1,209 +0,0 @@ -/* METADATA -{ - "name": "extended_http_tools", - - "display_name": { - "zh": "增强 HTTP 工具", - "en": "Extended HTTP Tools" - }, - "description": { - "zh": "允许文件上传,以及 GET/POST 等网络直接访问操作。", - "en": "Allows file uploads and direct network access operations such as GET/POST." - }, - "enabledByDefault": true, - "category": "Network", - "tools": [ - { - "name": "http_request", - "description": { "zh": "发送 HTTP 请求。", "en": "Send an HTTP request." }, - "parameters": [ - { "name": "url", "description": { "zh": "请求 URL", "en": "Request URL" }, "type": "string", "required": true }, - { "name": "method", "description": { "zh": "请求方法:GET/POST/PUT/DELETE", "en": "Method: GET/POST/PUT/DELETE" }, "type": "string", "required": true }, - { "name": "headers", "description": { "zh": "可选:headers(JSON 对象字符串)", "en": "Optional: headers (JSON object string)" }, "type": "string", "required": false }, - { "name": "body", "description": { "zh": "可选:请求体(字符串)", "en": "Optional: body (string)" }, "type": "string", "required": false }, - { "name": "body_type", "description": { "zh": "可选:json/form/text/xml", "en": "Optional: json/form/text/xml" }, "type": "string", "required": false }, - { "name": "ignore_ssl", "description": { "zh": "可选:是否忽略 HTTPS 证书校验(true/false)", "en": "Optional: ignore HTTPS certificate verification (true/false)" }, "type": "boolean", "required": false } - ] - }, - { - "name": "multipart_request", - "description": { "zh": "上传文件(multipart)。", "en": "Upload files (multipart)." }, - "parameters": [ - { "name": "url", "description": { "zh": "请求 URL", "en": "Request URL" }, "type": "string", "required": true }, - { "name": "method", "description": { "zh": "请求方法:POST/PUT", "en": "Method: POST/PUT" }, "type": "string", "required": true }, - { "name": "headers", "description": { "zh": "可选:headers(JSON 对象字符串)", "en": "Optional: headers (JSON object string)" }, "type": "string", "required": false }, - { "name": "form_data", "description": { "zh": "可选:form_data(字符串)", "en": "Optional: form_data (string)" }, "type": "string", "required": false }, - { "name": "files", "description": { "zh": "可选:files(JSON 数组字符串)", "en": "Optional: files (JSON array string)" }, "type": "string", "required": false }, - { "name": "ignore_ssl", "description": { "zh": "可选:是否忽略 HTTPS 证书校验(true/false)", "en": "Optional: ignore HTTPS certificate verification (true/false)" }, "type": "boolean", "required": false } - ] - }, - { - "name": "manage_cookies", - "description": { "zh": "管理 Cookies。", "en": "Manage cookies." }, - "parameters": [ - { "name": "action", "description": { "zh": "操作:get/set/clear", "en": "Action: get/set/clear" }, "type": "string", "required": true }, - { "name": "domain", "description": { "zh": "可选:域名", "en": "Optional: domain" }, "type": "string", "required": false }, - { "name": "cookies", "description": { "zh": "可选:cookies(字符串)", "en": "Optional: cookies (string)" }, "type": "string", "required": false } - ] - } - ] -}*/ - -const ExtendedHttpTools = (function () { - - const MAX_INLINE_HTTP_RESPONSE_CHARS = 12_000; - - interface ToolResponse { - success: boolean; - message: string; - data?: any; - } - - async function http_request(params: { - url: string; - method: string; - headers?: string; - body?: string; - body_type?: string; - ignore_ssl?: boolean; - }): Promise { - const toolParams: ToolParams = { - url: params.url, - method: params.method, - }; - if (params.headers !== undefined) toolParams.headers = params.headers; - if (params.body !== undefined) toolParams.body = params.body; - if (params.body_type !== undefined) toolParams.body_type = params.body_type; - if (params.ignore_ssl !== undefined) toolParams.ignore_ssl = params.ignore_ssl; - - const result = await toolCall({ name: "http_request", params: toolParams }); - const success = result.statusCode >= 200 && result.statusCode < 400; - - const contentStr = typeof result?.content === "string" ? result.content : ""; - if (contentStr.length > MAX_INLINE_HTTP_RESPONSE_CHARS) { - await Tools.Files.mkdir(OPERIT_CLEAN_ON_EXIT_DIR, true); - - const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); - const rand = Math.floor(Math.random() * 1_000_000); - - let ext = "txt"; - const ct = typeof result?.contentType === "string" ? result.contentType.toLowerCase() : ""; - if (ct.includes("json")) ext = "json"; - else if (ct.includes("html")) ext = "html"; - else if (ct.includes("xml")) ext = "xml"; - - const filePath = `${OPERIT_CLEAN_ON_EXIT_DIR}/http_response_${timestamp}_${rand}.${ext}`; - await Tools.Files.write(filePath, contentStr, false); - - const resultMeta = { - url: result?.url, - statusCode: result?.statusCode, - statusMessage: result?.statusMessage, - headers: result?.headers, - contentType: result?.contentType, - size: result?.size, - content: "(saved_to_file)", - }; - - return { - success, - message: `HTTP 请求完成;响应内容过大,已保存到文件:${filePath}。请使用 read_file_part 读取指定行范围,或用 grep_code 在该文件中检索关键字。`, - data: { - result: resultMeta, - content_saved_to: filePath, - operit_clean_on_exit_dir: OPERIT_CLEAN_ON_EXIT_DIR, - }, - }; - } - - return { success, message: 'HTTP 请求完成', data: result }; - } - - async function multipart_request(params: { - url: string; - method: string; - headers?: string; - form_data?: string; - files?: string; - ignore_ssl?: boolean; - }): Promise { - const toolParams: ToolParams = { - url: params.url, - method: params.method, - }; - if (params.headers !== undefined) toolParams.headers = params.headers; - if (params.form_data !== undefined) toolParams.form_data = params.form_data; - if (params.files !== undefined) toolParams.files = params.files; - if (params.ignore_ssl !== undefined) toolParams.ignore_ssl = params.ignore_ssl; - - const result = await toolCall({ name: "multipart_request", params: toolParams }); - const success = result.statusCode >= 200 && result.statusCode < 400; - return { success, message: '文件上传完成', data: result }; - } - - async function manage_cookies(params: { action: string; domain?: string; cookies?: string }): Promise { - const toolParams: ToolParams = { - action: params.action, - }; - if (params.domain !== undefined) toolParams.domain = params.domain; - if (params.cookies !== undefined) toolParams.cookies = params.cookies; - - const result = await toolCall({ name: "manage_cookies", params: toolParams }); - const success = result.statusCode >= 200 && result.statusCode < 400; - return { success, message: 'Cookies 操作完成', data: result }; - } - - async function wrapToolExecution

(func: (params: P) => Promise, params: P) { - try { - const result = await func(params); - complete(result); - } catch (error: any) { - console.error(`Tool ${func.name} failed unexpectedly`, error); - complete({ - success: false, - message: `工具执行时发生意外错误: ${error.message}`, - }); - } - } - - async function main() { - const results: any[] = []; - - // 这些工具可能对外发请求 / 写 cookies,默认不做自动化演示。 - results.push({ tool: 'http_request', result: { success: null, message: '未测试(会对外发起网络请求)' } }); - results.push({ tool: 'multipart_request', result: { success: null, message: '未测试(会上传文件/对外发起网络请求)' } }); - results.push({ tool: 'manage_cookies', result: { success: null, message: '未测试(会读写 cookies)' } }); - - complete({ - success: true, - message: "拓展 HTTP 工具包加载完成(未执行网络请求测试)", - data: { results } - }); - } - - return { - http_request: (params: { - url: string; - method: string; - headers?: string; - body?: string; - body_type?: string; - ignore_ssl?: boolean; - }) => wrapToolExecution(http_request, params), - multipart_request: (params: { - url: string; - method: string; - headers?: string; - form_data?: string; - files?: string; - ignore_ssl?: boolean; - }) => wrapToolExecution(multipart_request, params), - manage_cookies: (params: { action: string; domain?: string; cookies?: string }) => wrapToolExecution(manage_cookies, params), - main, - }; -})(); - -exports.http_request = ExtendedHttpTools.http_request; -exports.multipart_request = ExtendedHttpTools.multipart_request; -exports.manage_cookies = ExtendedHttpTools.manage_cookies; -exports.main = ExtendedHttpTools.main; diff --git a/scripts/extended_memory_tools.ts b/scripts/extended_memory_tools.ts deleted file mode 100644 index 05c645a..0000000 --- a/scripts/extended_memory_tools.ts +++ /dev/null @@ -1,305 +0,0 @@ -/* METADATA -{ - "name": "extended_memory_tools", - - "display_name": { - "zh": "增强记忆工具", - "en": "Extended Memory Tools" - }, - "description": { - "zh": "拓展记忆工具包:提供创建/更新/删除/查询/链接记忆,以及更新用户偏好的能力(默认工具中仅保留 query/get/query_links)。", - "en": "Extended memory tools: create/update/delete/query/link memories and update user preferences (default tools only keep query/get/query_links)." - }, - "category": "Memory", - "enabledByDefault": true, - "tools": [ - { - "name": "create_memory", - "description": { "zh": "创建新的记忆节点。", "en": "Create a new memory node." }, - "parameters": [ - { "name": "title", "description": { "zh": "记忆标题", "en": "Memory title" }, "type": "string", "required": true }, - { "name": "content", "description": { "zh": "记忆内容", "en": "Memory content" }, "type": "string", "required": true }, - { "name": "content_type", "description": { "zh": "可选:内容类型,默认 text/plain", "en": "Optional: content type (default: text/plain)" }, "type": "string", "required": false }, - { "name": "source", "description": { "zh": "可选:来源,默认 ai_created", "en": "Optional: source (default: ai_created)" }, "type": "string", "required": false }, - { "name": "folder_path", "description": { "zh": "可选:文件夹路径,默认空", "en": "Optional: folder path (default: empty)" }, "type": "string", "required": false }, - { "name": "tags", "description": { "zh": "可选:标签(逗号分隔字符串)", "en": "Optional: tags (comma-separated string)" }, "type": "string", "required": false } - ] - }, - { - "name": "update_memory", - "description": { "zh": "按标题更新已有记忆节点。", "en": "Update an existing memory node by title." }, - "parameters": [ - { "name": "old_title", "description": { "zh": "原始标题(用于定位记忆)", "en": "Old title (to locate the memory)" }, "type": "string", "required": true }, - { "name": "new_title", "description": { "zh": "可选:新标题(重命名)", "en": "Optional: new title (rename)" }, "type": "string", "required": false }, - { "name": "content", "description": { "zh": "可选:新内容", "en": "Optional: new content" }, "type": "string", "required": false }, - { "name": "content_type", "description": { "zh": "可选:内容类型", "en": "Optional: content type" }, "type": "string", "required": false }, - { "name": "source", "description": { "zh": "可选:来源", "en": "Optional: source" }, "type": "string", "required": false }, - { "name": "credibility", "description": { "zh": "可选:可信度 0-1", "en": "Optional: credibility 0-1" }, "type": "number", "required": false }, - { "name": "importance", "description": { "zh": "可选:重要性 0-1", "en": "Optional: importance 0-1" }, "type": "number", "required": false }, - { "name": "folder_path", "description": { "zh": "可选:文件夹路径", "en": "Optional: folder path" }, "type": "string", "required": false }, - { "name": "tags", "description": { "zh": "可选:标签(逗号分隔字符串)", "en": "Optional: tags (comma-separated string)" }, "type": "string", "required": false } - ] - }, - { - "name": "delete_memory", - "description": { "zh": "按标题删除记忆节点(不可逆)。", "en": "Delete a memory node by title (irreversible)." }, - "parameters": [ - { "name": "title", "description": { "zh": "要删除的记忆标题", "en": "Memory title to delete" }, "type": "string", "required": true } - ] - }, - { - "name": "move_memory", - "description": { "zh": "批量移动记忆到新文件夹。可按标题列表和/或来源文件夹筛选。", "en": "Move memories to another folder in batch. Filter by titles and/or source folder." }, - "parameters": [ - { "name": "target_folder_path", "description": { "zh": "目标文件夹路径(空字符串表示未分类)", "en": "Target folder path (empty string means uncategorized)" }, "type": "string", "required": true }, - { "name": "titles", "description": { "zh": "可选:标题列表(逗号或换行分隔)", "en": "Optional: title list (comma/newline separated)" }, "type": "string", "required": false }, - { "name": "source_folder_path", "description": { "zh": "可选:来源文件夹路径(空字符串表示未分类)", "en": "Optional: source folder path (empty string means uncategorized)" }, "type": "string", "required": false } - ] - }, - { - "name": "link_memories", - "description": { "zh": "创建两条记忆之间的语义链接。", "en": "Create a semantic link between two memories." }, - "parameters": [ - { "name": "source_title", "description": { "zh": "源记忆标题", "en": "Source memory title" }, "type": "string", "required": true }, - { "name": "target_title", "description": { "zh": "目标记忆标题", "en": "Target memory title" }, "type": "string", "required": true }, - { "name": "link_type", "description": { "zh": "可选:关系类型,默认 related", "en": "Optional: link type (default: related)" }, "type": "string", "required": false }, - { "name": "weight", "description": { "zh": "可选:强度 0-1,默认 0.7", "en": "Optional: weight 0-1 (default: 0.7)" }, "type": "number", "required": false }, - { "name": "description", "description": { "zh": "可选:关系描述", "en": "Optional: relationship description" }, "type": "string", "required": false } - ] - }, - { - "name": "query_memory_links", - "description": { "zh": "查询记忆链接(可按 ID/源标题/目标标题/关系类型过滤)。", "en": "Query memory links (filter by id/source/target/type)." }, - "parameters": [ - { "name": "link_id", "description": { "zh": "可选:链接ID", "en": "Optional: link id" }, "type": "number", "required": false }, - { "name": "source_title", "description": { "zh": "可选:源记忆标题", "en": "Optional: source memory title" }, "type": "string", "required": false }, - { "name": "target_title", "description": { "zh": "可选:目标记忆标题", "en": "Optional: target memory title" }, "type": "string", "required": false }, - { "name": "link_type", "description": { "zh": "可选:关系类型", "en": "Optional: link type" }, "type": "string", "required": false }, - { "name": "limit", "description": { "zh": "可选:返回上限 1-200,默认20", "en": "Optional: limit 1-200, default 20" }, "type": "number", "required": false } - ] - }, - { - "name": "update_memory_link", - "description": { "zh": "更新记忆链接(按 link_id 或 source/target/link_type 定位)。", "en": "Update a memory link (by link_id or source/target/link_type)." }, - "parameters": [ - { "name": "link_id", "description": { "zh": "可选:链接ID(优先使用)", "en": "Optional: link ID (preferred)" }, "type": "number", "required": false }, - { "name": "source_title", "description": { "zh": "可选:源记忆标题(未提供 link_id 时使用)", "en": "Optional: source title (used when link_id is not provided)" }, "type": "string", "required": false }, - { "name": "target_title", "description": { "zh": "可选:目标记忆标题(未提供 link_id 时使用)", "en": "Optional: target title (used when link_id is not provided)" }, "type": "string", "required": false }, - { "name": "link_type", "description": { "zh": "可选:当前关系类型(用于唯一定位)", "en": "Optional: current relation type (for unique resolution)" }, "type": "string", "required": false }, - { "name": "new_link_type", "description": { "zh": "可选:新的关系类型", "en": "Optional: new relation type" }, "type": "string", "required": false }, - { "name": "weight", "description": { "zh": "可选:新的强度 0-1", "en": "Optional: new weight 0-1" }, "type": "number", "required": false }, - { "name": "description", "description": { "zh": "可选:新的关系描述", "en": "Optional: new relationship description" }, "type": "string", "required": false } - ] - }, - { - "name": "delete_memory_link", - "description": { "zh": "删除记忆链接(按 link_id 或 source/target/link_type 定位)。", "en": "Delete a memory link (by link_id or source/target/link_type)." }, - "parameters": [ - { "name": "link_id", "description": { "zh": "可选:链接ID(优先使用)", "en": "Optional: link ID (preferred)" }, "type": "number", "required": false }, - { "name": "source_title", "description": { "zh": "可选:源记忆标题(未提供 link_id 时使用)", "en": "Optional: source title (used when link_id is not provided)" }, "type": "string", "required": false }, - { "name": "target_title", "description": { "zh": "可选:目标记忆标题(未提供 link_id 时使用)", "en": "Optional: target title (used when link_id is not provided)" }, "type": "string", "required": false }, - { "name": "link_type", "description": { "zh": "可选:关系类型(用于唯一定位)", "en": "Optional: relation type (for unique resolution)" }, "type": "string", "required": false } - ] - }, - { - "name": "update_user_preferences", - "description": { "zh": "更新用户偏好信息(至少提供一个字段)。", "en": "Update user preferences (provide at least one field)." }, - "parameters": [ - { "name": "birth_date", "description": { "zh": "可选:出生日期(Unix 毫秒时间戳)", "en": "Optional: birth date (Unix ms timestamp)" }, "type": "number", "required": false }, - { "name": "gender", "description": { "zh": "可选:性别", "en": "Optional: gender" }, "type": "string", "required": false }, - { "name": "personality", "description": { "zh": "可选:性格特征", "en": "Optional: personality" }, "type": "string", "required": false }, - { "name": "identity", "description": { "zh": "可选:身份/角色", "en": "Optional: identity/role" }, "type": "string", "required": false }, - { "name": "occupation", "description": { "zh": "可选:职业", "en": "Optional: occupation" }, "type": "string", "required": false }, - { "name": "ai_style", "description": { "zh": "可选:偏好 AI 交互风格", "en": "Optional: preferred AI interaction style" }, "type": "string", "required": false } - ] - } - ] -}*/ - -const ExtendedMemoryTools = (function () { - - interface ToolResponse { - success: boolean; - message: string; - data?: any; - } - - function resolveCallerCardId(): string | undefined { - if (typeof getCallerCardId !== 'function') { - return undefined; - } - const callerCardId = String(getCallerCardId() || '').trim(); - return callerCardId || undefined; - } - - async function create_memory(params: { title: string; content: string; content_type?: string; source?: string; folder_path?: string; tags?: string }): Promise { - const result = await Tools.Memory.create({ - title: params.title, - content: params.content, - contentType: params.content_type, - source: params.source, - folderPath: params.folder_path, - tags: params.tags, - callerCardId: resolveCallerCardId(), - }); - return { success: typeof result === 'string' && result.length > 0, message: '记忆创建完成', data: result }; - } - - async function update_memory(params: { old_title: string; new_title?: string; content?: string; content_type?: string; source?: string; credibility?: number; importance?: number; folder_path?: string; tags?: string }): Promise { - const result = await Tools.Memory.update({ - oldTitle: params.old_title, - newTitle: params.new_title, - content: params.content, - contentType: params.content_type, - source: params.source, - credibility: params.credibility, - importance: params.importance, - folderPath: params.folder_path, - tags: params.tags, - callerCardId: resolveCallerCardId(), - }); - return { success: typeof result === 'string' && result.length > 0, message: '记忆更新完成', data: result }; - } - - async function delete_memory(params: { title: string }): Promise { - const result = await Tools.Memory.deleteMemory({ - title: params.title, - callerCardId: resolveCallerCardId(), - }); - return { success: typeof result === 'string' && result.length > 0, message: '记忆删除完成', data: result }; - } - - async function move_memory(params: { target_folder_path: string; titles?: string; source_folder_path?: string }): Promise { - const titles = params.titles - ? params.titles.split(/[,\n|]/).map(s => s.trim()).filter(Boolean) - : undefined; - const result = await Tools.Memory.move({ - targetFolderPath: params.target_folder_path, - titles, - sourceFolderPath: params.source_folder_path, - callerCardId: resolveCallerCardId(), - }); - return { success: typeof result === 'string' && result.length > 0, message: '记忆移动完成', data: result }; - } - - async function link_memories(params: { source_title: string; target_title: string; link_type?: string; weight?: number; description?: string }): Promise { - const result = await Tools.Memory.link({ - sourceTitle: params.source_title, - targetTitle: params.target_title, - linkType: params.link_type, - weight: params.weight, - description: params.description, - callerCardId: resolveCallerCardId(), - }); - return { success: !!result, message: '记忆链接创建完成', data: result }; - } - - async function query_memory_links(params: { link_id?: number; source_title?: string; target_title?: string; link_type?: string; limit?: number }): Promise { - const result = await Tools.Memory.queryLinks({ - linkId: params.link_id, - sourceTitle: params.source_title, - targetTitle: params.target_title, - linkType: params.link_type, - limit: params.limit, - callerCardId: resolveCallerCardId(), - }); - return { success: !!result, message: '记忆链接查询完成', data: result }; - } - - async function update_memory_link(params: { link_id?: number; source_title?: string; target_title?: string; link_type?: string; new_link_type?: string; weight?: number; description?: string }): Promise { - const result = await Tools.Memory.updateLink({ - linkId: params.link_id, - sourceTitle: params.source_title, - targetTitle: params.target_title, - linkType: params.link_type, - newLinkType: params.new_link_type, - weight: params.weight, - description: params.description, - callerCardId: resolveCallerCardId(), - }); - return { success: !!result, message: '记忆链接更新完成', data: result }; - } - - async function delete_memory_link(params: { link_id?: number; source_title?: string; target_title?: string; link_type?: string }): Promise { - const result = await Tools.Memory.deleteLink({ - linkId: params.link_id, - sourceTitle: params.source_title, - targetTitle: params.target_title, - linkType: params.link_type, - callerCardId: resolveCallerCardId(), - }); - return { success: typeof result === 'string' ? result.length > 0 : !!result, message: '记忆链接删除完成', data: result }; - } - - async function update_user_preferences(params: { birth_date?: number; gender?: string; personality?: string; identity?: string; occupation?: string; ai_style?: string }): Promise { - const toolParams: ToolParams = {}; - if (params.birth_date !== undefined) toolParams.birth_date = params.birth_date; - if (params.gender !== undefined) toolParams.gender = params.gender; - if (params.personality !== undefined) toolParams.personality = params.personality; - if (params.identity !== undefined) toolParams.identity = params.identity; - if (params.occupation !== undefined) toolParams.occupation = params.occupation; - if (params.ai_style !== undefined) toolParams.ai_style = params.ai_style; - - const result = await toolCall({ name: "update_user_preferences", params: toolParams }); - const success = typeof result === 'string' ? result.length > 0 : !!result; - return { success, message: '用户偏好更新完成', data: result }; - } - - async function wrapToolExecution

(func: (params: P) => Promise, params: P) { - try { - const result = await func(params); - complete(result); - } catch (error: any) { - console.error(`Tool ${func.name} failed unexpectedly`, error); - complete({ - success: false, - message: `工具执行时发生意外错误: ${error.message}`, - }); - } - } - - async function main() { - const results: any[] = []; - - // 这些工具都可能修改记忆/偏好,默认不做自动化演示,避免污染用户数据。 - results.push({ tool: 'create_memory', result: { success: null, message: '未测试(会写入记忆库)' } }); - results.push({ tool: 'update_memory', result: { success: null, message: '未测试(会修改记忆库)' } }); - results.push({ tool: 'delete_memory', result: { success: null, message: '未测试(会删除记忆库数据)' } }); - results.push({ tool: 'move_memory', result: { success: null, message: '未测试(会批量修改记忆文件夹)' } }); - results.push({ tool: 'link_memories', result: { success: null, message: '未测试(会修改记忆库链接)' } }); - results.push({ tool: 'query_memory_links', result: { success: null, message: '未测试(只读查询)' } }); - results.push({ tool: 'update_memory_link', result: { success: null, message: '未测试(会修改记忆库链接)' } }); - results.push({ tool: 'delete_memory_link', result: { success: null, message: '未测试(会删除记忆库链接)' } }); - results.push({ tool: 'update_user_preferences', result: { success: null, message: '未测试(会修改用户偏好)' } }); - - complete({ - success: true, - message: "拓展记忆工具包加载完成(未执行破坏性测试)", - data: { results } - }); - } - - return { - create_memory: (params: { title: string; content: string; content_type?: string; source?: string; folder_path?: string; tags?: string }) => wrapToolExecution(create_memory, params), - update_memory: (params: { old_title: string; new_title?: string; content?: string; content_type?: string; source?: string; credibility?: number; importance?: number; folder_path?: string; tags?: string }) => wrapToolExecution(update_memory, params), - delete_memory: (params: { title: string }) => wrapToolExecution(delete_memory, params), - move_memory: (params: { target_folder_path: string; titles?: string; source_folder_path?: string }) => wrapToolExecution(move_memory, params), - link_memories: (params: { source_title: string; target_title: string; link_type?: string; weight?: number; description?: string }) => wrapToolExecution(link_memories, params), - query_memory_links: (params: { link_id?: number; source_title?: string; target_title?: string; link_type?: string; limit?: number }) => wrapToolExecution(query_memory_links, params), - update_memory_link: (params: { link_id?: number; source_title?: string; target_title?: string; link_type?: string; new_link_type?: string; weight?: number; description?: string }) => wrapToolExecution(update_memory_link, params), - delete_memory_link: (params: { link_id?: number; source_title?: string; target_title?: string; link_type?: string }) => wrapToolExecution(delete_memory_link, params), - update_user_preferences: (params: { birth_date?: number; gender?: string; personality?: string; identity?: string; occupation?: string; ai_style?: string }) => wrapToolExecution(update_user_preferences, params), - main, - }; -})(); - -exports.create_memory = ExtendedMemoryTools.create_memory; -exports.update_memory = ExtendedMemoryTools.update_memory; -exports.delete_memory = ExtendedMemoryTools.delete_memory; -exports.move_memory = ExtendedMemoryTools.move_memory; -exports.link_memories = ExtendedMemoryTools.link_memories; -exports.query_memory_links = ExtendedMemoryTools.query_memory_links; -exports.update_memory_link = ExtendedMemoryTools.update_memory_link; -exports.delete_memory_link = ExtendedMemoryTools.delete_memory_link; -exports.update_user_preferences = ExtendedMemoryTools.update_user_preferences; -exports.main = ExtendedMemoryTools.main; diff --git a/scripts/ffmpeg.ts b/scripts/ffmpeg.ts deleted file mode 100644 index ffe5cf9..0000000 --- a/scripts/ffmpeg.ts +++ /dev/null @@ -1,143 +0,0 @@ -/* METADATA -{ - "name": "ffmpeg", - - "display_name": { - "zh": "FFmpeg 工具集", - "en": "FFmpeg Toolkit" - }, - "description": { - "zh": "提供FFmpeg工具,用于处理多媒体内容。", - "en": "FFmpeg utilities for processing multimedia content." - }, - "enabledByDefault": true, - "category": "Media", - "tools": [ - { - "name": "ffmpeg_execute", - "description": { "zh": "执行自定义FFmpeg命令(仅填写参数,不要包含前缀 ffmpeg)。", "en": "Execute a custom FFmpeg command (arguments only; do not include the leading ffmpeg)." }, - "parameters": [ - { "name": "command", "description": { "zh": "要执行的FFmpeg命令参数(不要包含前缀 ffmpeg)", "en": "FFmpeg command arguments to execute (do not include the leading ffmpeg)" }, "type": "string", "required": true } - ] - }, - { - "name": "ffmpeg_info", - "description": { "zh": "获取FFmpeg系统信息,包括版本、构建配置和支持的编解码器。", "en": "Get FFmpeg system info, including version, build config, and supported codecs." }, - "parameters": [] - }, - { - "name": "ffmpeg_convert", - "description": { "zh": "使用简化参数转换视频文件。", "en": "Convert a video file using simplified parameters." }, - "parameters": [ - { "name": "input_path", "description": { "zh": "源视频文件路径", "en": "Input video file path" }, "type": "string", "required": true }, - { "name": "output_path", "description": { "zh": "目标视频文件路径", "en": "Output video file path" }, "type": "string", "required": true }, - { "name": "video_codec", "description": { "zh": "可选,要使用的视频编解码器。推荐使用 'h264'。支持的值: 'h264', 'hevc', 'vp8', 'vp9', 'av1', 'libx265', 'libvpx', 'libaom', 'mpeg4', 'mjpeg', 'prores'。", "en": "Optional. Video codec to use. Prefer 'h264'. Supported values: 'h264', 'hevc', 'vp8', 'vp9', 'av1', 'libx265', 'libvpx', 'libaom', 'mpeg4', 'mjpeg', 'prores'." }, "type": "string", "required": false }, - { "name": "audio_codec", "description": { "zh": "可选,要使用的音频编解码器。支持的值: 'aac', 'mp3', 'opus', 'vorbis', 'flac', 'pcm', 'wav', 'ac3', 'eac3'。", "en": "Optional. Audio codec to use. Supported values: 'aac', 'mp3', 'opus', 'vorbis', 'flac', 'pcm', 'wav', 'ac3', 'eac3'." }, "type": "string", "required": false }, - { "name": "resolution", "description": { "zh": "可选,输出分辨率,例如 '1280x720'。", "en": "Optional. Output resolution, e.g. '1280x720'." }, "type": "string", "required": false }, - { "name": "bitrate", "description": { "zh": "可选,视频比特率,例如 '1000k'。", "en": "Optional. Video bitrate, e.g. '1000k'." }, "type": "string", "required": false } - ] - } - ] -}*/ - -const FFmpegTools = (function () { - - type FFmpegVideoCodec = 'h264' | 'hevc' | 'vp8' | 'vp9' | 'av1' | 'libx265' | 'libvpx' | 'libaom' | 'mpeg4' | 'mjpeg' | 'prores'; - type FFmpegAudioCodec = 'aac' | 'mp3' | 'opus' | 'vorbis' | 'flac' | 'pcm' | 'wav' | 'ac3' | 'eac3'; - type FFmpegResolution = '1280x720' | '1920x1080' | '3840x2160' | '7680x4320' | `${number}x${number}`; - type FFmpegBitrate = '500k' | '1000k' | '2000k' | '4000k' | '8000k' | `${number}k` | `${number}M`; - - interface ToolResponse { - success: boolean; - message: string; - data?: any; - } - - async function ffmpeg_execute(params: { command: string }): Promise { - const result = await Tools.FFmpeg.execute(params.command); - return { - success: result.returnCode === 0, - message: result.returnCode === 0 ? "FFmpeg command executed successfully." : `FFmpeg command failed with return code ${result.returnCode}.`, - data: result.output - }; - } - - async function ffmpeg_info(): Promise { - const result = await Tools.FFmpeg.info(); - return { - success: result.returnCode === 0, - message: result.returnCode === 0 ? "FFmpeg info retrieved successfully." : "Failed to retrieve FFmpeg info.", - data: result.output - }; - } - - async function ffmpeg_convert(params: { - input_path: string, - output_path: string, - video_codec?: FFmpegVideoCodec, - audio_codec?: FFmpegAudioCodec, - resolution?: FFmpegResolution, - bitrate?: FFmpegBitrate - }): Promise { - const result = await Tools.FFmpeg.convert(params.input_path, params.output_path, { - video_codec: params.video_codec, - audio_codec: params.audio_codec, - resolution: params.resolution, - bitrate: params.bitrate, - }); - return { - success: result.returnCode === 0, - message: result.returnCode === 0 ? "FFmpeg conversion completed successfully." : `FFmpeg conversion failed with return code ${result.returnCode}.`, - data: result.output - }; - } - - async function wrapToolExecution

(func: (params: P) => Promise, params: P) { - try { - const result = await func(params); - complete(result); - } catch (error: any) { - console.error(`Tool ${func.name} failed unexpectedly`, error); - complete({ - success: false, - message: `工具执行时发生意外错误: ${error.message}`, - }); - } - } - - async function main() { - console.log("--- FFmpeg Tools Test ---"); - - console.log("\n[1/3] Testing ffmpeg_info..."); - const infoResult = await ffmpeg_info(); - console.log(JSON.stringify(infoResult, null, 2)); - - console.log("\n[2/3] Testing ffmpeg_execute (example: getting help for a decoder)..."); - const executeResult = await ffmpeg_execute({ command: '-h decoder=h264' }); - console.log(JSON.stringify(executeResult, null, 2)); - - console.log("\n[3/3] Testing ffmpeg_convert (example)..."); - console.log("Skipping ffmpeg_convert test as it requires input files."); - - complete({ success: true, message: "Test finished." }); - } - - return { - ffmpeg_execute: (params: { command: string }) => wrapToolExecution(ffmpeg_execute, params), - ffmpeg_info: (params: {}) => wrapToolExecution(ffmpeg_info, params), - ffmpeg_convert: (params: { - input_path: string, - output_path: string, - video_codec?: FFmpegVideoCodec, - audio_codec?: FFmpegAudioCodec, - resolution?: FFmpegResolution, - bitrate?: FFmpegBitrate - }) => wrapToolExecution(ffmpeg_convert, params), - main, - }; -})(); - -exports.ffmpeg_execute = FFmpegTools.ffmpeg_execute; -exports.ffmpeg_info = FFmpegTools.ffmpeg_info; -exports.ffmpeg_convert = FFmpegTools.ffmpeg_convert; -exports.main = FFmpegTools.main; \ No newline at end of file diff --git a/scripts/file_converter.ts b/scripts/file_converter.ts deleted file mode 100644 index 229dcbf..0000000 --- a/scripts/file_converter.ts +++ /dev/null @@ -1,265 +0,0 @@ -/* METADATA -{ - "name": "file_converter", - - "display_name": { - "zh": "文件转换器", - "en": "File Converter" - }, - "description": { - "zh": "提供全面的文件格式转换功能。支持常见的音频/视频(如 MP4、MOV、MP3、WAV)、图像(如 JPG、PNG、WEBP)以及文档(如 Markdown、HTML、DOCX、PDF)之间的相互转换。", - "en": "Comprehensive file format conversion. Supports converting between common audio/video (MP4, MOV, MP3, WAV), images (JPG, PNG, WEBP), and documents (Markdown, HTML, DOCX, PDF)." - }, - "enabledByDefault": true, - "category": "File", - "tools": [ - { - "name": "convert_file", - "description": { - "zh": "调用 FFmpeg、ImageMagick 和 Pandoc 等外部命令行工具,转换文件的格式。支持音视频、图像和文档等多种类型。如果检测到工具未安装,会尝试自动安装。", - "en": "Convert files using external CLI tools such as FFmpeg, ImageMagick, and Pandoc. Supports audio/video, images, and documents. If a required tool is missing, it will attempt to install it automatically." - }, - "parameters": [ - { "name": "input_path", "description": { "zh": "输入文件的路径。", "en": "Input file path." }, "type": "string", "required": true }, - { "name": "output_path", "description": { "zh": "输出文件的路径。扩展名决定了目标格式。", "en": "Output file path. The file extension determines the target format." }, "type": "string", "required": true }, - { "name": "options", "description": { "zh": "用于转换工具的可选命令行选项 (例如, 为 ImageMagick 设置 '-quality 80' )。", "en": "Optional CLI options for the conversion tool (e.g. set '-quality 80' for ImageMagick)." }, "type": "string", "required": false } - ] - } - ] -}*/ - -const fileConverter = (function () { - - let terminalSessionId: string | null = null; - - async function getTerminalSessionId(): Promise { - if (terminalSessionId) { - return terminalSessionId; - } - const session = await Tools.System.terminal.create("file_converter_session"); - terminalSessionId = session.sessionId; - return terminalSessionId; - } - - async function executeTerminalCommand(command: string, timeoutMs?: number) { - const sessionId = await getTerminalSessionId(); - return await Tools.System.terminal.exec(sessionId, command); - } - - interface ToolResponse { - success: boolean; - message: string; - data?: any; - } - - interface ConvertFileParams { - input_path: string; - output_path: string; - options?: string; - } - - /** - * Checks if a command-line tool is installed, and attempts to install it if not found. - * @param toolName The name of the command (e.g., "ffmpeg"). - * @param packageName The name of the package to install (e.g., "ffmpeg"). - * @returns A promise that resolves to true if the tool is available. - * @throws An error if the tool is not found and installation fails. - */ - async function checkAndInstall(toolName: string, packageName: string): Promise { - console.log(`Checking for ${toolName}...`); - const checkCmd = `command -v ${toolName}`; - const checkResult = await executeTerminalCommand(checkCmd); - - if (checkResult.exitCode === 0 && checkResult.output.trim() !== '') { - console.log(`${toolName} is already installed at: ${checkResult.output.trim()}`); - return true; - } - - console.log(`${toolName} not found. Attempting to install package: ${packageName}...`); - - // Assuming an apt-based system (like Debian/Ubuntu). - const updateCmd = 'apt-get update'; - console.log(`Running: ${updateCmd}`); - const updateResult = await executeTerminalCommand(updateCmd); - if (updateResult.exitCode !== 0) { - console.warn(`'apt-get update' failed. This might be okay if caches are fresh, but installation may fail.\nOutput: ${updateResult.output}`); - } - - const installCmd = `apt-get install -y ${packageName}`; - console.log(`Running: ${installCmd}`); - const installResult = await executeTerminalCommand(installCmd); - - if (installResult.exitCode !== 0) { - console.error(`Failed to install ${packageName}: ${installResult.output}`); - throw new Error(`Failed to install required tool: ${toolName} (package: ${packageName}). Please try installing it manually.`); - } - - console.log(`${packageName} installed successfully.`); - return true; - } - - /** - * Determines the appropriate conversion tool and command based on file extensions. - * @param inputPath Path to the input file. - * @param outputPath Path to the output file. - * @param options Additional options for the command. - * @returns An object with tool information and the command to execute. - */ - function getConverterInfo(inputPath: string, outputPath: string, options?: string): { command: string; tool: string; pkg: string; } { - const getExt = (path: string) => path.split('.').pop()?.toLowerCase() || ''; - const inputExt = getExt(inputPath); - const outputExt = getExt(outputPath); - - const isAudioVideo = (ext: string) => ['mp4', 'mkv', 'avi', 'mov', 'flv', 'webm', 'mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a', 'wma', 'wmv'].includes(ext); - const isImage = (ext: string) => ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'tiff', 'ico', 'svg'].includes(ext); - const isDocument = (ext: string) => ['md', 'html', 'docx', 'pdf', 'txt', 'epub', 'odt', 'rtf', 'tex', 'rst', 'json', 'csv'].includes(ext); - - if (isAudioVideo(inputExt) || isAudioVideo(outputExt)) { - return { - tool: 'ffmpeg', - pkg: 'ffmpeg', - command: `ffmpeg -y -i "${inputPath}" ${options || ''} "${outputPath}"` - }; - } - - if (isImage(inputExt) || isImage(outputExt)) { - return { - tool: 'convert', - pkg: 'imagemagick', - command: `convert "${inputPath}" ${options || ''} "${outputPath}"` - }; - } - - if (isDocument(inputExt) || isDocument(outputExt)) { - return { - tool: 'pandoc', - pkg: 'pandoc', - command: `pandoc "${inputPath}" -o "${outputPath}" ${options || ''}` - }; - } - - throw new Error(`Unsupported or ambiguous file conversion from .${inputExt} to .${outputExt}.`); - } - - /** - * The core logic for the convert_file tool. - * @param params Parameters for the conversion. - * @returns An object with the result of the conversion. - */ - async function convert_file(params: ConvertFileParams) { - const { input_path, output_path, options } = params; - - const fileExists = await Tools.Files.exists(input_path); - if (!fileExists.exists) { - throw new Error(`Input file not found: ${input_path}`); - } - - const converter = getConverterInfo(input_path, output_path, options); - - await checkAndInstall(converter.tool, converter.pkg); - - console.log(`Executing conversion command: ${converter.command}`); - const result = await executeTerminalCommand(converter.command); - - if (result.exitCode !== 0) { - throw new Error(`Conversion failed. Exit code: ${result.exitCode}\nOutput:\n${result.output}`); - } - - const outputExists = await Tools.Files.exists(output_path); - if (!outputExists.exists) { - // Sometimes a tool exits with 0 but fails, writing to stderr. - throw new Error(`Conversion process finished, but output file was not created at: ${output_path}\nTerminal Output:\n${result.output}`); - } - - return { - output_path: output_path, - details: `File converted successfully and saved to ${output_path}.`, - terminal_output: result.output - }; - } - - /** - * A wrapper function for executing tools to provide standardized success/error handling. - */ - async function wrap(func: (params: any) => Promise, params: any, successMessage: string, failMessage: string) { - try { - const result = await func(params); - complete({ success: true, message: successMessage, data: result }); - } catch (error: any) { - console.error(`Function ${func.name} failed! Error: ${error.message}`); - complete({ success: false, message: `${failMessage}: ${error.message}`, error_stack: error.stack }); - } - } - - /** - * A main function for self-testing the capabilities of this tool package. - */ - async function main() { - console.log("--- Starting File Converter Tool Test ---"); - const testDir = "/sdcard/Download/converter_test"; - await Tools.Files.mkdir(testDir, true); - - // Test 1: Image conversion (PNG to JPG) - try { - console.log("\n[1/3] Testing Image Conversion (PNG -> JPG)"); - // A simple 1x1 red pixel PNG in base64 - const pngBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/wcAAwAB/epv2AAAAABJRU5ErkJggg=="; - const inputPng = `${testDir}/test.png`; - const outputJpg = `${testDir}/test.jpg`; - await Tools.Files.writeBinary(inputPng, pngBase64); - - const imageResult = await convert_file({ input_path: inputPng, output_path: outputJpg }); - console.log("Image conversion success:", imageResult); - if (!(await Tools.Files.exists(outputJpg)).exists) { - throw new Error("JPG file not created."); - } - - // Verify that readBinary can read the converted image as Base64 - const jpgBinary = await Tools.Files.readBinary(outputJpg); - console.log("ReadBinary success: size=", jpgBinary.size, "bytes, base64 length=", jpgBinary.contentBase64.length); - } catch (e: any) { - console.error("Image conversion test failed:", e.message); - } - - // Test 2: Document conversion (MD to HTML) - try { - console.log("\n[2/3] Testing Document Conversion (MD -> HTML)"); - const inputMd = `${testDir}/test.md`; - const outputHtml = `${testDir}/test.html`; - await Tools.Files.write(inputMd, "# Hello World"); - - const docResult = await convert_file({ input_path: inputMd, output_path: outputHtml }); - console.log("Document conversion success:", docResult); - const htmlContent = (await Tools.Files.read(outputHtml)).content; - if (!htmlContent.includes(" tar)"); - const inputZip = `${testDir}/test.zip`; - await Tools.Files.write(inputZip, "dummy content"); - await convert_file({ input_path: inputZip, output_path: `${testDir}/test.tar` }); - console.error("Unsupported conversion test FAILED: It should have thrown an error but didn't."); - } catch (e: any) { - console.log("Unsupported conversion test PASSED as expected:", e.message); - } - - console.log("\n--- File Converter Tool Test Finished ---"); - await Tools.Files.deleteFile(testDir, true); - console.log("Cleaned up test directory."); - complete({ success: true, message: "All tests finished." }); - } - - return { - convert_file: (p: ConvertFileParams) => wrap(convert_file, p, 'File conversion successful.', 'File conversion failed.'), - main: main, - }; -})(); - -exports.convert_file = fileConverter.convert_file; -exports.main = fileConverter.main; diff --git a/scripts/google_search.ts b/scripts/google_search.ts deleted file mode 100644 index f860f4a..0000000 --- a/scripts/google_search.ts +++ /dev/null @@ -1,257 +0,0 @@ -/* METADATA -{ - "name": "google_search", - - "display_name": { - "zh": "Google 搜索", - "en": "Google Search" - }, - "description": { - "zh": "提供 Google 普通搜索与 Google Scholar 学术搜索能力,支持设置语言与返回条数。", - "en": "Google web search and Google Scholar search, with configurable language and result count." - }, - "enabledByDefault": false, - "category": "Search", - "tools": [ - { - "name": "search_web", - "description": { "zh": "执行 Google 普通搜索,返回网页搜索结果。", "en": "Perform a regular Google search and return web results." }, - "parameters": [ - { "name": "query", "description": { "zh": "搜索关键词", "en": "Search query" }, "type": "string", "required": true }, - { "name": "max_results", "description": { "zh": "返回结果数量,默认 10,最大 20", "en": "Number of results to return (default: 10, max: 20)" }, "type": "number", "required": false }, - { "name": "language", "description": { "zh": "界面语言参数,默认 en", "en": "Interface language (default: en)" }, "type": "string", "required": false }, - { "name": "region", "description": { "zh": "地区参数,例如 us、cn。默认 us", "en": "Region parameter, e.g. us, cn (default: us)" }, "type": "string", "required": false }, - { "name": "includeLinks", "description": { "zh": "是否在结果中包含可点击的链接列表,默认为false。", "en": "Whether to include a clickable link list in results (default: false)" }, "type": "boolean", "required": false } - ] - }, - { - "name": "search_scholar", - "description": { "zh": "执行 Google Scholar 学术搜索,返回学术文献结果。", "en": "Perform a Google Scholar search and return academic results." }, - "parameters": [ - { "name": "query", "description": { "zh": "搜索关键词", "en": "Search query" }, "type": "string", "required": true }, - { "name": "max_results", "description": { "zh": "返回结果数量,默认 10,最大 20", "en": "Number of results to return (default: 10, max: 20)" }, "type": "number", "required": false }, - { "name": "language", "description": { "zh": "界面语言参数,默认 en", "en": "Interface language (default: en)" }, "type": "string", "required": false }, - { "name": "includeLinks", "description": { "zh": "是否在结果中包含可点击的链接列表,默认为false。", "en": "Whether to include a clickable link list in results (default: false)" }, "type": "boolean", "required": false } - ] - }, - { - "name": "search_scholar_mirror", - "description": { "zh": "通过镜像站执行 Google Scholar 学术搜索,以绕过人机验证。", "en": "Use a mirror site to perform Google Scholar searches to bypass CAPTCHA checks." }, - "parameters": [ - { "name": "query", "description": { "zh": "搜索关键词", "en": "Search query" }, "type": "string", "required": true }, - { "name": "max_results", "description": { "zh": "返回结果数量,默认 10,最大 20", "en": "Number of results to return (default: 10, max: 20)" }, "type": "number", "required": false }, - { "name": "language", "description": { "zh": "界面语言参数,默认 en", "en": "Interface language (default: en)" }, "type": "string", "required": false }, - { "name": "includeLinks", "description": { "zh": "是否在结果中包含可点击的链接列表,默认为false。", "en": "Whether to include a clickable link list in results (default: false)" }, "type": "boolean", "required": false } - ] - } - ] -}*/ - -const googleSearch = (function () { - type SearchParams = { - query: string; - max_results?: number; - language?: string; - region?: string; - includeLinks?: boolean; - }; - - type ScholarSearchParams = { - query: string; - max_results?: number; - language?: string; - includeLinks?: boolean; - }; - - type GoogleSearchResult = { - title: string; - url: string; - snippet: string; - position: number; - }; - - const GOOGLE_SEARCH_URL = "https://www.google.com/search"; - const GOOGLE_SCHOLAR_URL = "https://scholar.google.com/scholar"; - const GOOGLE_SCHOLAR_MIRRORS = [ - "https://xs.cntpj.com/scholar", - ]; - const MAX_RESULTS = 20; - - function buildUrl(base: string, params: Record): string { - const queryString = Object.entries(params) - .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) - .join("&"); - return `${base}?${queryString}`; - } - - function getHostname(rawUrl: string): string { - const match = rawUrl.trim().match(/^[a-zA-Z][a-zA-Z\d+\-.]*:\/\/([^/?#]+)/); - if (!match || !match[1]) { - return rawUrl; - } - const authority = match[1]; - const host = authority.includes("@") ? authority.split("@").pop() || authority : authority; - return host.replace(/:\d+$/, ""); - } - - async function fetchHtmlViaWebVisit(url: string): Promise { - const result = await Tools.Net.visit(url); - // The result can be a string if the underlying tool returns a simple string. - // We'll normalize it to a VisitWebResultData object. - if (typeof result === "string") { - return { - url: url, - title: "", - content: result, - links: [], - toString: () => result, - }; - } - return result; - } - - // 解析相关逻辑已移除,直接返回 visit 的纯文本结果 - - async function performSearch(url: string, includeLinks: boolean = false, sourceName: string) { - try { - const result = await fetchHtmlViaWebVisit(url); - const content = result.content || ''; - - let parts: string[] = []; - if (result.visitKey) { - parts.push(`visit_key: ${result.visitKey}`); - } - if (includeLinks && result.links && result.links.length > 0) { - const linksLines = result.links.map((link, index) => `[${index + 1}] ${link.text}`); - parts.push(linksLines.join('\n')); - } - parts.push(content); - - return { - success: true, - message: `${sourceName} 搜索成功`, - data: parts.join('\n\n') - }; - } catch (error: any) { - return { - success: false, - message: `${sourceName} 搜索失败: ${error.message}` - }; - } - } - - async function searchWeb(params: SearchParams) { - if (!params.query || params.query.trim() === "") { - throw new Error("请提供有效的 query 参数。"); - } - const maxResults = Math.min(Math.max(params.max_results || 10, 1), MAX_RESULTS); - const language = params.language || "en"; - const region = params.region || "us"; - const url = buildUrl(GOOGLE_SEARCH_URL, { - q: params.query, - hl: language, - gl: region, - num: String(maxResults), - pws: "0", - }); - - return performSearch(url, params.includeLinks, "Google"); - } - - async function searchScholar(params: ScholarSearchParams) { - if (!params.query || params.query.trim() === "") { - throw new Error("请提供有效的 query 参数。"); - } - const maxResults = Math.min(Math.max(params.max_results || 10, 1), MAX_RESULTS); - const language = params.language || "en"; - const url = buildUrl(GOOGLE_SCHOLAR_URL, { - q: params.query, - hl: language, - as_sdt: "0,5", - num: String(maxResults) - }); - - return performSearch(url, params.includeLinks, "Google Scholar"); - } - - async function searchScholarMirror(params: ScholarSearchParams) { - if (!params.query || params.query.trim() === "") { - throw new Error("请提供有效的 query 参数。"); - } - const maxResults = Math.min(Math.max(params.max_results || 10, 1), MAX_RESULTS); - const language = params.language || "en"; - - const mirrorUrls = GOOGLE_SCHOLAR_MIRRORS.map(mirror => buildUrl(mirror, { - q: params.query, - hl: language, - as_sdt: "0,5", - num: String(maxResults) - })); - - if (mirrorUrls.length === 0) { - return { - success: false, - message: "没有可用的 Google Scholar 镜像地址。" - }; - } - - let lastError: any = null; - - for (const currentUrl of mirrorUrls) { - try { - const searchResult = await performSearch(currentUrl, params.includeLinks, `Google Scholar 镜像 (${getHostname(currentUrl)})`); - if (searchResult.success && searchResult.data) { - // Check for CAPTCHA in the content - if (searchResult.data.includes("recaptcha") || searchResult.data.includes("人机身份验证")) { - throw new Error("CAPTCHA required"); - } - return searchResult; - } - // If performSearch itself fails, it will throw and be caught below. - } catch (error: any) { - lastError = error; - console.log(`Attempt with ${currentUrl} failed: ${error.message}`); - } - } - - return { - success: false, - message: `Google Scholar 镜像搜索在尝试所有镜像后失败: ${lastError?.message || 'Unknown error'}` - }; - } - - async function main() { - console.log("--- Testing Web Search ---"); - const webResult = await searchWeb({ query: "TypeScript" }); - console.log(JSON.stringify(webResult, null, 2)); - - console.log("\n--- Testing Scholar Search ---"); - const scholarResult = await searchScholar({ query: "Large Language Models" }); - console.log(JSON.stringify(scholarResult, null, 2)); - - console.log("\n--- Testing Scholar Mirror Search ---"); - const scholarMirrorResult = await searchScholarMirror({ query: "Quantum Computing" }); - console.log(JSON.stringify(scholarMirrorResult, null, 2)); - } - - function wrap(coreFunction: (params: any) => Promise) { - return async (params: any) => { - // The core function expects the params object directly. - return coreFunction(params); - }; - } - - return { - search_web: searchWeb, - search_scholar: searchScholar, - search_scholar_mirror: searchScholarMirror, - main, - wrap, - }; -})(); - -exports.search_web = googleSearch.wrap(googleSearch.search_web); -exports.search_scholar = googleSearch.wrap(googleSearch.search_scholar); -exports.search_scholar_mirror = googleSearch.wrap(googleSearch.search_scholar_mirror); -exports.main = googleSearch.main; - diff --git a/scripts/hex_editor.ts b/scripts/hex_editor.ts deleted file mode 100644 index 0247d1f..0000000 --- a/scripts/hex_editor.ts +++ /dev/null @@ -1,443 +0,0 @@ -/* METADATA -{ - "name": "hex_editor", - "display_name": { - "zh": "十六进制编辑器", - "en": "Hex Editor" - }, - "description": { - "zh": "一个面向二进制文件的轻量十六进制查看与修改工具包,支持查看指定偏移的 hex 窗口、按偏移写入字节序列,以及搜索指定 hex 模式。", - "en": "A lightweight binary hex inspection and patching toolkit. It can read hex windows at offsets, write byte sequences at offsets, and search for hex patterns." - }, - "enabledByDefault": false, - "category": "File", - "tools": [ - { - "name": "read_hex_window", - "description": { - "zh": "读取二进制文件在指定偏移附近的十六进制窗口,并附带 ASCII 预览,便于快速定位和人工核对。", - "en": "Read a hex window from a binary file at a specific offset, including an ASCII preview for quick inspection." - }, - "parameters": [ - { "name": "file_path", "description": { "zh": "目标文件路径。", "en": "Target file path." }, "type": "string", "required": true }, - { "name": "offset", "description": { "zh": "起始偏移,可写十进制或 0x 开头的十六进制。", "en": "Start offset in decimal or 0x-prefixed hexadecimal." }, "type": "string", "required": true }, - { "name": "length", "description": { "zh": "读取字节数,默认 128,最大 4096。", "en": "Number of bytes to read. Default 128, max 4096." }, "type": "number", "required": false }, - { "name": "environment", "description": { "zh": "文件环境:android 或 linux,默认 android。", "en": "File environment: android or linux. Defaults to android." }, "type": "string", "required": false } - ] - }, - { - "name": "write_hex_bytes", - "description": { - "zh": "把给定的 hex 字节序列直接写入文件指定偏移。该工具会原地修改文件,不会自动生成回退文件。", - "en": "Write a hex byte sequence directly into a file at the specified offset. This patches the file in place and does not create rollback files." - }, - "parameters": [ - { "name": "file_path", "description": { "zh": "目标文件路径。", "en": "Target file path." }, "type": "string", "required": true }, - { "name": "offset", "description": { "zh": "写入起始偏移,可写十进制或 0x 开头的十六进制。", "en": "Write offset in decimal or 0x-prefixed hexadecimal." }, "type": "string", "required": true }, - { "name": "hex_bytes", "description": { "zh": "要写入的 hex 字节序列,例如 'DE AD BE EF' 或 'deadbeef'。", "en": "Hex bytes to write, for example 'DE AD BE EF' or 'deadbeef'." }, "type": "string", "required": true }, - { "name": "environment", "description": { "zh": "文件环境:android 或 linux,默认 android。", "en": "File environment: android or linux. Defaults to android." }, "type": "string", "required": false } - ] - }, - { - "name": "find_hex_pattern", - "description": { - "zh": "在二进制文件中搜索指定的 hex 模式,返回命中偏移以及每个命中点附近的十六进制窗口。", - "en": "Search a binary file for a hex pattern and return matched offsets with nearby hex windows." - }, - "parameters": [ - { "name": "file_path", "description": { "zh": "目标文件路径。", "en": "Target file path." }, "type": "string", "required": true }, - { "name": "hex_bytes", "description": { "zh": "要搜索的 hex 模式,例如 '50 4B 03 04'。", "en": "Hex pattern to search for, for example '50 4B 03 04'." }, "type": "string", "required": true }, - { "name": "max_results", "description": { "zh": "最多返回多少个命中,默认 20,最大 100。", "en": "Maximum number of matches to return. Default 20, max 100." }, "type": "number", "required": false }, - { "name": "environment", "description": { "zh": "文件环境:android 或 linux,默认 android。", "en": "File environment: android or linux. Defaults to android." }, "type": "string", "required": false } - ] - } - ] -}*/ - -type FileEnvironment = "android" | "linux"; - -interface ReadHexWindowParams { - file_path: string; - offset: string; - length?: number; - environment?: string; -} - -interface WriteHexBytesParams { - file_path: string; - offset: string; - hex_bytes: string; - environment?: string; -} - -interface FindHexPatternParams { - file_path: string; - hex_bytes: string; - max_results?: number; - environment?: string; -} - -const hexEditorPackage = (function () { - const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - const DEFAULT_WINDOW_LENGTH = 128; - const MAX_WINDOW_LENGTH = 4096; - const DEFAULT_MAX_RESULTS = 20; - const MAX_RESULTS_LIMIT = 100; - const CONTEXT_BEFORE = 16; - const CONTEXT_TOTAL = 64; - const BYTES_PER_LINE = 16; - - function normalizeEnvironment(environment?: string): FileEnvironment { - const value = String(environment || "android").trim().toLowerCase(); - if (value !== "android" && value !== "linux") { - throw new Error("environment 只能是 android 或 linux。"); - } - return value as FileEnvironment; - } - - function normalizeBase64Input(value: string): string { - const cleaned = String(value || "") - .replace(/\s+/g, "") - .replace(/-/g, "+") - .replace(/_/g, "/"); - if (!cleaned) { - return ""; - } - const remainder = cleaned.length % 4; - if (remainder === 1) { - throw new Error("Invalid base64 string"); - } - return remainder === 0 ? cleaned : cleaned + "=".repeat(4 - remainder); - } - - function decodeBase64ToBytes(value: string): number[] { - const normalized = normalizeBase64Input(value); - const bytes: number[] = []; - - for (let i = 0; i < normalized.length; i += 4) { - const c1 = normalized[i]; - const c2 = normalized[i + 1]; - const c3 = normalized[i + 2]; - const c4 = normalized[i + 3]; - - const v1 = BASE64_ALPHABET.indexOf(c1); - const v2 = BASE64_ALPHABET.indexOf(c2); - const v3 = c3 === "=" ? 0 : BASE64_ALPHABET.indexOf(c3); - const v4 = c4 === "=" ? 0 : BASE64_ALPHABET.indexOf(c4); - - if (v1 < 0 || v2 < 0 || (c3 !== "=" && v3 < 0) || (c4 !== "=" && v4 < 0)) { - throw new Error("Invalid base64 string"); - } - - const chunk = (v1 << 18) | (v2 << 12) | (v3 << 6) | v4; - bytes.push((chunk >> 16) & 0xff); - if (c3 !== "=") { - bytes.push((chunk >> 8) & 0xff); - } - if (c4 !== "=") { - bytes.push(chunk & 0xff); - } - } - - return bytes; - } - - function encodeBytesToBase64(bytes: number[]): string { - let output = ""; - - for (let i = 0; i < bytes.length; i += 3) { - const b1 = bytes[i] ?? 0; - const hasB2 = i + 1 < bytes.length; - const hasB3 = i + 2 < bytes.length; - const b2 = hasB2 ? (bytes[i + 1] ?? 0) : 0; - const b3 = hasB3 ? (bytes[i + 2] ?? 0) : 0; - const chunk = (b1 << 16) | (b2 << 8) | b3; - - output += BASE64_ALPHABET[(chunk >> 18) & 0x3f]; - output += BASE64_ALPHABET[(chunk >> 12) & 0x3f]; - output += hasB2 ? BASE64_ALPHABET[(chunk >> 6) & 0x3f] : "="; - output += hasB3 ? BASE64_ALPHABET[chunk & 0x3f] : "="; - } - - return output; - } - - function parseOffset(value: string): number { - const trimmed = String(value || "").trim().toLowerCase(); - if (!trimmed) { - throw new Error("offset 不能为空。"); - } - - let parsed = NaN; - if (/^0x[0-9a-f]+$/.test(trimmed)) { - parsed = parseInt(trimmed.slice(2), 16); - } else if (/^\d+$/.test(trimmed)) { - parsed = parseInt(trimmed, 10); - } else { - throw new Error("offset 必须是十进制数字或 0x 开头的十六进制。"); - } - - if (!Number.isFinite(parsed) || parsed < 0) { - throw new Error("offset 必须是大于等于 0 的整数。"); - } - return parsed; - } - - function clampPositiveInteger(value: number | undefined, defaultValue: number, maxValue: number, fieldName: string): number { - const actual = value == null ? defaultValue : Math.floor(value); - if (!Number.isFinite(actual) || actual <= 0) { - throw new Error(`${fieldName} 必须是大于 0 的整数。`); - } - return Math.min(actual, maxValue); - } - - function normalizeHexBytesInput(value: string): string { - const trimmed = String(value || "").trim(); - if (!trimmed) { - throw new Error("hex_bytes 不能为空。"); - } - const withoutPrefix = trimmed.replace(/^0x/i, ""); - const normalized = withoutPrefix.replace(/[^0-9a-fA-F]/g, "").toLowerCase(); - if (!normalized) { - throw new Error("hex_bytes 中没有可用的十六进制字符。"); - } - if (normalized.length % 2 !== 0) { - throw new Error("hex_bytes 必须包含偶数个十六进制字符。"); - } - return normalized; - } - - function parseHexBytes(value: string): number[] { - const normalized = normalizeHexBytesInput(value); - const bytes: number[] = []; - for (let i = 0; i < normalized.length; i += 2) { - bytes.push(parseInt(normalized.slice(i, i + 2), 16)); - } - return bytes; - } - - function toHex(value: number, width: number): string { - let hex = Math.max(0, Number(value) || 0).toString(16).toUpperCase(); - while (hex.length < width) { - hex = `0${hex}`; - } - return hex; - } - - function bytesToHexString(bytes: number[]): string { - return bytes.map((byte) => toHex(byte, 2)).join(" "); - } - - function bytesToAscii(bytes: number[]): string { - return bytes - .map((byte) => { - if (byte >= 32 && byte <= 126) { - return String.fromCharCode(byte); - } - return "."; - }) - .join(""); - } - - function buildHexDump(bytes: number[], startOffset: number): string { - if (bytes.length === 0) { - return ""; - } - - const lines: string[] = []; - const offsetWidth = Math.max(8, (startOffset + bytes.length).toString(16).length); - - for (let index = 0; index < bytes.length; index += BYTES_PER_LINE) { - const slice = bytes.slice(index, index + BYTES_PER_LINE); - const hexCells = slice.map((byte) => toHex(byte, 2)); - while (hexCells.length < BYTES_PER_LINE) { - hexCells.push(" "); - } - const left = hexCells.slice(0, 8).join(" "); - const right = hexCells.slice(8).join(" "); - const ascii = bytesToAscii(slice).padEnd(BYTES_PER_LINE, " "); - lines.push(`${toHex(startOffset + index, offsetWidth)} ${left} ${right} |${ascii}|`); - } - - return lines.join("\n"); - } - - async function ensureFileExists(path: string, environment: FileEnvironment): Promise { - const exists = await Tools.Files.exists(path, environment); - if (!exists.exists) { - throw new Error(`文件不存在: ${path}`); - } - if (exists.isDirectory) { - throw new Error(`目标路径是目录,不是文件: ${path}`); - } - } - - async function readFileBytes(path: string, environment: FileEnvironment): Promise<{ base64: string; bytes: number[]; size: number }> { - await ensureFileExists(path, environment); - const result = await Tools.Files.readBinary(path, environment); - const base64 = String(result.contentBase64 || ""); - return { - base64, - bytes: decodeBase64ToBytes(base64), - size: Number(result.size || 0) - }; - } - - async function writeFileBytes(path: string, bytes: number[], environment: FileEnvironment) { - return await Tools.Files.writeBinary(path, encodeBytesToBase64(bytes), environment); - } - - function collectWindow(bytes: number[], offset: number, length: number): { window: number[]; actualLength: number } { - if (offset > bytes.length) { - throw new Error(`offset 超出文件范围。文件大小为 ${bytes.length} 字节。`); - } - const end = Math.min(bytes.length, offset + length); - const window = bytes.slice(offset, end); - return { - window, - actualLength: window.length - }; - } - - async function read_hex_window(params: ReadHexWindowParams) { - const environment = normalizeEnvironment(params.environment); - const offset = parseOffset(params.offset); - const length = clampPositiveInteger(params.length, DEFAULT_WINDOW_LENGTH, MAX_WINDOW_LENGTH, "length"); - const file = await readFileBytes(params.file_path, environment); - const { window, actualLength } = collectWindow(file.bytes, offset, length); - - return { - file_path: params.file_path, - environment, - file_size: file.size, - offset_decimal: offset, - offset_hex: `0x${toHex(offset, 8)}`, - requested_length: length, - actual_length: actualLength, - end_offset_exclusive_decimal: offset + actualLength, - end_offset_exclusive_hex: `0x${toHex(offset + actualLength, 8)}`, - hex: bytesToHexString(window), - ascii_preview: bytesToAscii(window), - hex_dump: buildHexDump(window, offset) - }; - } - - async function write_hex_bytes(params: WriteHexBytesParams) { - const environment = normalizeEnvironment(params.environment); - const offset = parseOffset(params.offset); - const patchBytes = parseHexBytes(params.hex_bytes); - const file = await readFileBytes(params.file_path, environment); - - if (offset + patchBytes.length > file.bytes.length) { - throw new Error(`写入范围超出文件大小。文件大小 ${file.bytes.length} 字节,尝试写入到 ${offset + patchBytes.length}。`); - } - - const beforeBytes = file.bytes.slice(offset, offset + patchBytes.length); - for (let i = 0; i < patchBytes.length; i += 1) { - file.bytes[offset + i] = patchBytes[i]; - } - - const writeResult = await writeFileBytes(params.file_path, file.bytes, environment); - const afterBytes = file.bytes.slice(offset, offset + patchBytes.length); - - return { - file_path: params.file_path, - environment, - offset_decimal: offset, - offset_hex: `0x${toHex(offset, 8)}`, - bytes_written: patchBytes.length, - before_hex: bytesToHexString(beforeBytes), - after_hex: bytesToHexString(afterBytes), - write_result: writeResult - }; - } - - async function find_hex_pattern(params: FindHexPatternParams) { - const environment = normalizeEnvironment(params.environment); - const patternBytes = parseHexBytes(params.hex_bytes); - const maxResults = clampPositiveInteger(params.max_results, DEFAULT_MAX_RESULTS, MAX_RESULTS_LIMIT, "max_results"); - const file = await readFileBytes(params.file_path, environment); - const matches: Array<{ - offset_decimal: number; - offset_hex: string; - matched_hex: string; - context_hex_dump: string; - }> = []; - - if (patternBytes.length > file.bytes.length) { - return { - file_path: params.file_path, - environment, - file_size: file.size, - pattern_hex: bytesToHexString(patternBytes), - match_count: 0, - matches - }; - } - - for (let offset = 0; offset <= file.bytes.length - patternBytes.length; offset += 1) { - let matched = true; - for (let index = 0; index < patternBytes.length; index += 1) { - if (file.bytes[offset + index] !== patternBytes[index]) { - matched = false; - break; - } - } - if (!matched) { - continue; - } - - const contextStart = Math.max(0, offset - CONTEXT_BEFORE); - const contextEnd = Math.min(file.bytes.length, contextStart + CONTEXT_TOTAL); - const contextBytes = file.bytes.slice(contextStart, contextEnd); - - matches.push({ - offset_decimal: offset, - offset_hex: `0x${toHex(offset, 8)}`, - matched_hex: bytesToHexString(patternBytes), - context_hex_dump: buildHexDump(contextBytes, contextStart) - }); - - if (matches.length >= maxResults) { - break; - } - } - - return { - file_path: params.file_path, - environment, - file_size: file.size, - pattern_hex: bytesToHexString(patternBytes), - match_count: matches.length, - matches - }; - } - - async function wrap(fn: (params: any) => Promise, params: any, successMessage: string, failureMessage: string) { - try { - const data = await fn(params); - complete({ - success: true, - message: successMessage, - data - }); - } catch (error: any) { - console.error(`[hex_editor] ${fn.name} failed:`, error); - complete({ - success: false, - message: `${failureMessage}: ${error.message}`, - error_stack: error.stack - }); - } - } - - return { - read_hex_window: (params: ReadHexWindowParams) => wrap(read_hex_window, params, "Hex 窗口读取成功。", "Hex 窗口读取失败"), - write_hex_bytes: (params: WriteHexBytesParams) => wrap(write_hex_bytes, params, "Hex 字节写入成功。", "Hex 字节写入失败"), - find_hex_pattern: (params: FindHexPatternParams) => wrap(find_hex_pattern, params, "Hex 模式搜索成功。", "Hex 模式搜索失败") - }; -})(); - -exports.read_hex_window = hexEditorPackage.read_hex_window; -exports.write_hex_bytes = hexEditorPackage.write_hex_bytes; -exports.find_hex_pattern = hexEditorPackage.find_hex_pattern; diff --git a/scripts/jmcomic.ts b/scripts/jmcomic.ts deleted file mode 100644 index 29bdf06..0000000 --- a/scripts/jmcomic.ts +++ /dev/null @@ -1,986 +0,0 @@ -/* -METADATA -{ - "name": "jmcomic_downloader", - - "display_name": { - "zh": "JMComic 下载器", - "en": "JMComic Downloader" - }, - "description": { "zh": "提供JMComic漫画下载功能,支持搜索、获取信息和下载,包括对新漫画的图片反爬解码。", "en": "JMComic downloader: search comics, fetch details, and download albums. Includes anti-crawling image decoding for newer comics." }, - "category": "Media", - "tools": [ - { - "name": "main", - "description": { "zh": "运行一个内置的测试函数,以验证JMComic工具的基本功能(搜索和获取信息)是否正常工作。", "en": "Run a built-in test to verify basic JMComic functionality (search and info retrieval)." }, - "parameters": [] - }, - { - "name": "search_comics", - "description": { "zh": "搜索JMComic漫画", "en": "Search JMComic comics." }, - "parameters": [ - { - "name": "query", - "description": { "zh": "搜索关键词", "en": "Search keyword." }, - "type": "string", - "required": true - }, - { - "name": "page", - "description": { "zh": "页码 (默认: 1)", "en": "Page number (default: 1)." }, - "type": "number", - "required": false - }, - { - "name": "order_by", - "description": { "zh": "排序方式 (latest, view, picture, like, 默认: view)", "en": "Sort mode: latest/view/picture/like (default: view)." }, - "type": "string", - "required": false - }, - { - "name": "time", - "description": { "zh": "时间范围 (today, week, month, all, 默认: all)", "en": "Time range: today/week/month/all (default: all)." }, - "type": "string", - "required": false - } - ] - }, - { - "name": "get_album_info", - "description": { "zh": "获取漫画(本子)的详细信息", "en": "Get detailed information for a comic album." }, - "parameters": [ - { - "name": "album_id", - "description": { "zh": "漫画ID", "en": "Album ID." }, - "type": "string", - "required": true - } - ] - }, - { - "name": "download_album", - "description": { "zh": "下载指定ID的单本漫画,包含图片解码功能。", "en": "Download a single comic album by ID, including image decoding." }, - "parameters": [ - { - "name": "album_id", - "description": { "zh": "要下载的漫画ID", "en": "Album ID to download." }, - "type": "string", - "required": true - }, - { - "name": "download_dir", - "description": { "zh": "下载目录 (可选, 默认: /sdcard/Download/Operit/plugins/jmcomic_downloader/downloads)", "en": "Download directory (optional; default: /sdcard/Download/Operit/plugins/jmcomic_downloader/downloads)." }, - "type": "string", - "required": false - } - ] - }, - { - "name": "batch_download_albums", - "description": { "zh": "批量下载多本漫画,包含图片解码功能。", "en": "Batch download multiple comic albums, including image decoding." }, - "parameters": [ - { - "name": "album_ids", - "description": { "zh": "要下载的漫画ID列表,用逗号分隔", "en": "Comma-separated list of album IDs to download." }, - "type": "string", - "required": true - }, - { - "name": "download_dir", - "description": { "zh": "下载目录 (可选, 默认: /sdcard/Download/Operit/plugins/jmcomic_downloader/downloads)", "en": "Download directory (optional; default: /sdcard/Download/Operit/plugins/jmcomic_downloader/downloads)." }, - "type": "string", - "required": false - } - ] - } - ], - "enabledByDefault": false -}*/ - -// region Type Definitions -interface Album { - id: string; - title: string; - author: string; - scrambleId: number; - episodeList: { id: string; title: string }[]; - length: number; -} - -interface Photo { - id: string; - title: string; - albumId: string; - scrambleId: number; - pageArr: string[]; - length: number; -} - -interface SearchResultItem { - id: string; - title: string; -} - -interface SearchResult { - search_params: SearchParams; - results: SearchResultItem[]; - total_results: number; -} - -interface SearchParams { - query: string; - page?: number; - order_by?: 'latest' | 'view' | 'picture' | 'like'; - time?: 'today' | 'week' | 'month' | 'all'; -} - -interface AlbumInfo { - id: string; - title: string; - author: string; - chapterCount: number; - success: boolean; -} - -interface DownloadResult { - success: boolean; - albumId: string; - title?: string; - downloadedFiles?: { - directory: string | undefined; - fileCount: number; - files: string[]; - }; - error?: string; -} - -interface BatchDownloadResult { - success: boolean; - albumId: string; - title?: string; - error?: string; -} - -// endregion - -const jmcomic = (function () { - const DEFAULT_DOWNLOAD_DIR = `${getPluginConfigDir("jmcomic_downloader")}/downloads`; - const TEST_DOWNLOAD_DIR = `${getPluginConfigDir("jmcomic_downloader")}/test_downloads`; - - // region Polyfill & Utils - const BASE64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; - - function normalizeBase64Input(value: string): string { - const cleaned = String(value || '') - .replace(/\s+/g, '') - .replace(/-/g, '+') - .replace(/_/g, '/'); - if (!cleaned) { - return ''; - } - const remainder = cleaned.length % 4; - if (remainder === 1) { - throw new Error('Invalid base64 string'); - } - return remainder === 0 ? cleaned : cleaned + '='.repeat(4 - remainder); - } - - function decodeBase64ToBytes(value: string): number[] { - const normalized = normalizeBase64Input(value); - const bytes: number[] = []; - - for (let i = 0; i < normalized.length; i += 4) { - const c1 = normalized[i]; - const c2 = normalized[i + 1]; - const c3 = normalized[i + 2]; - const c4 = normalized[i + 3]; - - const v1 = BASE64_ALPHABET.indexOf(c1); - const v2 = BASE64_ALPHABET.indexOf(c2); - const v3 = c3 === '=' ? 0 : BASE64_ALPHABET.indexOf(c3); - const v4 = c4 === '=' ? 0 : BASE64_ALPHABET.indexOf(c4); - - if (v1 < 0 || v2 < 0 || (c3 !== '=' && v3 < 0) || (c4 !== '=' && v4 < 0)) { - throw new Error('Invalid base64 string'); - } - - const chunk = (v1 << 18) | (v2 << 12) | (v3 << 6) | v4; - bytes.push((chunk >> 16) & 0xff); - if (c3 !== '=') { - bytes.push((chunk >> 8) & 0xff); - } - if (c4 !== '=') { - bytes.push(chunk & 0xff); - } - } - - return bytes; - } - - function encodeBytesToBase64(bytes: number[]): string { - let output = ''; - - for (let i = 0; i < bytes.length; i += 3) { - const b1 = bytes[i]; - const hasB2 = i + 1 < bytes.length; - const hasB3 = i + 2 < bytes.length; - const b2 = hasB2 ? bytes[i + 1] : 0; - const b3 = hasB3 ? bytes[i + 2] : 0; - const chunk = (b1 << 16) | (b2 << 8) | b3; - - output += BASE64_ALPHABET[(chunk >> 18) & 0x3f]; - output += BASE64_ALPHABET[(chunk >> 12) & 0x3f]; - output += hasB2 ? BASE64_ALPHABET[(chunk >> 6) & 0x3f] : '='; - output += hasB3 ? BASE64_ALPHABET[chunk & 0x3f] : '='; - } - - return output; - } - - function decodeBase64Binary(value: string): string { - return decodeBase64ToBytes(value) - .map((byte) => String.fromCharCode(byte)) - .join(''); - } - - function encodeBase64Binary(value: string): string { - const bytes: number[] = []; - const input = String(value || ''); - for (let i = 0; i < input.length; i++) { - bytes.push(input.charCodeAt(i) & 0xff); - } - return encodeBytesToBase64(bytes); - } - - // Buffer a subset of Buffer functionality for base64 encoding/decoding - const Buffer = { - from: (str: string, encoding: string = 'utf8') => { - if (encoding === 'base64') { - return decodeBase64Binary(str); - } else if (encoding === 'hex') { - let s = ''; - for (let i = 0; i < str.length; i += 2) { - s += String.fromCharCode(parseInt(str.substr(i, 2), 16)); - } - return s; - } - return str; - }, - toString: (buf: string, encoding: string = 'utf8') => { - if (encoding === 'base64') { - return encodeBase64Binary(buf); - } else if (encoding === 'hex') { - let s = ''; - for (let i = 0; i < buf.length; i++) { - s += ('0' + buf.charCodeAt(i).toString(16)).slice(-2); - } - return s; - } - return buf; - } - }; - - function joinPath(...segments: string[]): string { - return segments.join('/').replace(/\/+/g, '/'); - } - - function dirname(filePath: string): string { - const lastSlashPos = filePath.lastIndexOf('/'); - if (lastSlashPos === -1) { - return "."; - } - if (lastSlashPos === 0) { - return "/"; - } - return filePath.substring(0, lastSlashPos); - } - - async function ensureDirExists(dirPath: string): Promise { - if (!dirPath || dirPath === '/' || dirPath === '.') { - return; - } - - const dirExists = await Tools.Files.exists(dirPath); - if (dirExists.exists) { - return; - } - - const parentDir = dirname(dirPath); - await ensureDirExists(parentDir); - - const dirStillNotExists = await Tools.Files.exists(dirPath); - if (!dirStillNotExists.exists) { - await Tools.Files.mkdir(dirPath); - } - } - - function basename(filePath: string): string { - return filePath.substring(filePath.lastIndexOf('/') + 1); - } - - async function runTasksWithConcurrency(tasks: (() => Promise)[], limit: number): Promise { - const results: (T | Error)[] = new Array(tasks.length); - let currentIndex = 0; - - async function runner(): Promise { - while (currentIndex < tasks.length) { - const taskIndex = currentIndex++; - if (taskIndex < tasks.length) { - try { - results[taskIndex] = await tasks[taskIndex](); - } catch (e: any) { - console.error(`并发任务 ${taskIndex} 执行失败: ${e.message}`); - results[taskIndex] = e; - } - } - } - } - - const runners: Promise[] = []; - const numRunners = Math.min(limit, tasks.length); - for (let i = 0; i < numRunners; i++) { - runners.push(runner()); - } - - await Promise.all(runners); - return results.filter(r => !(r instanceof Error)) as T[]; - } - - // endregion - - // region Constants and Classes from jmcomic - const __version__ = '2.6.4-ts-adapted'; - - function shuffleDomains(domains: string[]): string[] { - const shuffled = [...domains]; - for (let i = shuffled.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; - } - return shuffled; - } - - const JmMagicConstants = { - APP_TOKEN_SECRET: '18comicAPP', - APP_TOKEN_SECRET_2: '18comicAPPContent', - APP_DATA_SECRET: '185Hcomic3PAPP7R', - APP_VERSION: '1.8.0', - SCRAMBLE_220980: 220980, - SCRAMBLE_268850: 268850, - SCRAMBLE_421926: 421926 - }; - - const JmModuleConfig = { - PROT: 'https://', - DOMAIN_API_LIST: shuffleDomains([ - 'www.cdnmhwscc.vip', - 'www.cdnplaystation6.club', - 'www.cdnplaystation6.org', - 'www.cdnuc.vip', - 'www.cdn-mspjmapiproxy.xyz' - ]), - DOMAIN_IMAGE_LIST: shuffleDomains([ - 'cdn-msp.jmapiproxy1.cc', - 'cdn-msp.jmapiproxy2.cc', - 'cdn-msp2.jmapiproxy2.cc', - 'cdn-msp3.jmapiproxy2.cc' - ]), - APP_HEADERS_TEMPLATE: { - 'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7', - 'X-Requested-With': 'com.jiaohua_browser', - 'user-agent': 'Mozilla/5.0 (Linux; Android 9; V1938CT Build/PQ3A.190705.11211812; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/91.0.4472.114 Safari/537.36', - }, - APP_HEADERS_IMAGE: { - 'Accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8', - 'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7', - 'X-Requested-With': 'com.jiaohua_browser', - } - }; - - class JmImageTool { - static getNum(scrambleId: number | string, photoId: number | string, imageName: string): number { - const scrambleIdNum = parseInt(scrambleId.toString()); - const photoIdNum = parseInt(photoId.toString()); - - if (photoIdNum < scrambleIdNum) { - return 0; - } else if (photoIdNum < JmMagicConstants.SCRAMBLE_268850) { - return 10; - } else { - const x = photoIdNum < JmMagicConstants.SCRAMBLE_421926 ? 10 : 8; - - const imageNameWithoutExt = this.getFileNameFromUrl(imageName, true); - const s = `${photoIdNum}${imageNameWithoutExt}`; - - const hash = CryptoJS.MD5(s).toString(); - const lastChar = hash.charCodeAt(hash.length - 1); - const num = lastChar % x; - return (num * 2) + 2; - } - } - - static getFileNameFromUrl(url: string, withoutExtension: boolean = true): string { - const queryIndex = url.indexOf('?'); - if (queryIndex !== -1) { - url = url.substring(0, queryIndex); - } - const filename = basename(url); - if (withoutExtension) { - const lastDotIndex = filename.lastIndexOf('.'); - return lastDotIndex !== -1 ? filename.substring(0, lastDotIndex) : filename; - } - return filename; - } - - static async decodeAndSave(num: number, imageBase64: string, decodedSavePath: string): Promise { - if (num === 0) { - await Tools.Files.writeBinary(decodedSavePath, imageBase64); - return; - } - - let srcImage: Jimp.JimpWrapper | undefined = undefined; - let resultImage: Jimp.JimpWrapper | undefined = undefined; - const stripsToRelease: Jimp.JimpWrapper[] = []; - - try { - srcImage = await Jimp.read(imageBase64); - const w = await srcImage.getWidth(); - const h = await srcImage.getHeight(); - const over = h % num; - - resultImage = await Jimp.create(w, h); - - for (let i = 0; i < num; i++) { - let move = Math.floor(h / num); - let ySrc = h - (move * (i + 1)) - over; - let yDst = move * i; - - if (i === 0) { - move += over; - } else { - yDst += over; - } - - if (ySrc < 0 || move <= 0 || (ySrc + move > h)) continue; - - const strip = await srcImage.crop(0, ySrc, w, move); - stripsToRelease.push(strip); - await resultImage.composite(strip, 0, yDst); - } - - const decodedImageBase64 = await resultImage.getBase64(Jimp.MIME_JPEG); - - // 移除 "data:image/jpeg;base64," 前缀 - const pureBase64 = decodedImageBase64.substring(decodedImageBase64.indexOf(',') + 1); - - await Tools.Files.writeBinary(decodedSavePath, pureBase64); - } catch (e: any) { - console.error(`图片解码失败,将保存原始图片: ${e.message}`); - await Tools.Files.writeBinary(decodedSavePath, imageBase64); - } finally { - if (srcImage) await srcImage.release(); - if (resultImage) await resultImage.release(); - for (const strip of stripsToRelease) { - await strip.release(); - } - } - } - } - - class JmCryptoTool { - static md5hex(key: string): string { - return CryptoJS.MD5(key).toString(); - } - - static tokenAndTokenparam(ts: number, secret: string = JmMagicConstants.APP_TOKEN_SECRET): [string, string] { - const tokenparam = `${ts},${JmMagicConstants.APP_VERSION}`; - const token = this.md5hex(`${ts}${secret}`); - return [token, tokenparam]; - } - - static decodeRespData(data: string, ts: number, secret: string = JmMagicConstants.APP_DATA_SECRET): string { - try { - // Revert to the standard CryptoJS usage pattern. - // First, create the key from ts and secret using MD5. - const keyHex = this.md5hex(`${ts}${secret}`); - const key = CryptoJS.enc.Hex.parse(keyHex); - - // Then, call decrypt with the standard signature. - const decrypted = CryptoJS.AES.decrypt(data, key, { - mode: CryptoJS.mode.ECB, - padding: CryptoJS.pad.Pkcs7 - }); - - const decryptedText = decrypted.toString(CryptoJS.enc.Utf8); - - if (!decryptedText) { - throw new Error("AES decryption returned an empty result."); - } - return decryptedText; - } catch (error: any) { - console.error(`Underlying decryption error: ${error.message}`); - throw new Error(`AES Decryption failed. Original error: ${error.message}`); - } - } - } - - class JmApiResp { - constructor(private resp: OkHttpResponse, private ts: number) { } - - get isSuccess(): boolean { - return this.resp.isSuccessful(); - } - - get json(): any { - try { - return JSON.parse(this.resp.content); - } catch (error: any) { - throw new Error(`JSON解析失败: ${error.message}`); - } - } - - get isSuccessful(): boolean { - return this.isSuccess && this.json.code === 200; - } - - get encodedData(): string { - return this.json.data; - } - - get decodedData(): string { - return JmCryptoTool.decodeRespData(this.encodedData, this.ts); - } - - get resData(): any { - if (!this.isSuccessful) { - throw new Error(`API请求失败: code=${this.json.code}`); - } - const decoded = this.decodedData; - try { - if (typeof decoded !== 'string' || !decoded) { - throw new Error(`Cannot parse non-string or empty value. Type: ${typeof decoded}`); - } - return JSON.parse(decoded); - } catch (error: any) { - const preview = String(decoded || 'N/A').substring(0, 80); - throw new Error(`Failed to parse decrypted response. Error: ${error.message}. Original data type was ${typeof decoded}.`); - } - } - - get modelData(): any { - return this.resData; - } - } - - class DirRuleImpl { - constructor(public baseDir: string) { } - - decideImageSaveDir(album: Album, photo: Photo): string { - return joinPath(this.baseDir, this.sanitize(album.title)); - } - - decideAlbumRootDir(album: Pick): string { - return joinPath(this.baseDir, this.sanitize(album.title)); - } - - sanitize(name: string): string { - return name.replace(/[\\?%*:|"<>]/g, '_'); - } - } - - class JmOptionImpl { - public dirRule: DirRuleImpl; - constructor(baseDir: string = DEFAULT_DOWNLOAD_DIR) { - this.dirRule = new DirRuleImpl(baseDir); - } - - static default(baseDir: string = DEFAULT_DOWNLOAD_DIR): JmOptionImpl { - return new JmOptionImpl(baseDir); - } - - buildJmClient(): JmApiClientImpl { - return new JmApiClientImpl(); - } - } - - class JmApiClientImpl { - private domainList: string[] = JmModuleConfig.DOMAIN_API_LIST; - private retryTimes: number = 3; - private client: OkHttpClient = OkHttp.newClient(); - - private API_ALBUM = '/album'; - private API_CHAPTER = '/chapter'; - private API_SEARCH = '/search'; - private API_CATEGORIES_FILTER = '/categories/filter'; - - async getAlbumDetail(albumId: string): Promise { - const resp = await this.reqApi(`${this.API_ALBUM}?id=${albumId}`); - const data = resp.resData; - if (!data || !data.name) throw new Error(`本子 ${albumId} 不存在或数据无效`); - return this.parseAlbumData(albumId, data); - } - - async getPhotoDetail(photoId: string): Promise { - const resp = await this.reqApi(`${this.API_CHAPTER}?id=${photoId}`); - const data = resp.resData; - if (!data || !data.name) throw new Error(`章节 ${photoId} 不存在或数据无效`); - return this.parsePhotoData(photoId, data); - } - - async searchComics(params: SearchParams): Promise { - const { query, page = 1, order_by = 'view', time = 'all' } = params; - const orderMap: { [key: string]: string } = { 'latest': 'mr', 'view': 'mv', 'picture': 'mp', 'like': 'tf' }; - const timeMap: { [key: string]: string } = { 'today': 't', 'week': 'w', 'month': 'm', 'all': 'a' }; - const apiParams = { - search_query: query, - page, - o: orderMap[order_by.toLowerCase()] || orderMap['view'], - t: timeMap[time.toLowerCase()] || timeMap['all'] - }; - const resp = await this.reqApi(`${this.API_SEARCH}?${this.toUrlSearchParams(apiParams)}`); - const data = resp.resData; - const results = (data.content || []).map((item: any): SearchResultItem => ({ - id: String(item.id || item.album_id), - title: item.name || item.title - })); - return { - search_params: params, - results: results, - total_results: results.length, - }; - } - - async reqApi(url: string, method: 'GET' | 'POST' = 'GET', data?: any): Promise { - const ts = Math.floor(Date.now() / 1000); - for (let i = 0; i < this.domainList.length; i++) { - const domain = this.domainList[i]; - for (let retry = 0; retry < this.retryTimes; retry++) { - try { - const fullUrl = `${JmModuleConfig.PROT}${domain}${url}`; - const [token, tokenparam] = JmCryptoTool.tokenAndTokenparam(ts); - const headers = { ...JmModuleConfig.APP_HEADERS_TEMPLATE, token, tokenparam }; - - const requestBuilder = this.client.newRequest().url(fullUrl).headers(headers); - if (method === 'POST') { - requestBuilder.method('POST').jsonBody(data); - } - - const resp = await requestBuilder.build().execute(); - - if (resp.isSuccessful()) { - return new JmApiResp(resp, ts); - } - } catch (error: any) { - console.log(`[API] 请求失败: ${error.message} 域名: ${domain}`); - if (retry === this.retryTimes - 1 && i === this.domainList.length - 1) { - throw new Error(`所有域名和重试都失败: ${error.message}`); - } - } - } - } - throw new Error('请求失败'); - } - - async downloadImage(imageUrl: string, savePath: string, scrambleId: number | string, photoId: string): Promise { - try { - const response = await this.client.newRequest().url(imageUrl).headers(JmModuleConfig.APP_HEADERS_IMAGE).build().execute(); - - if (!response.isSuccessful()) { - throw new Error(`HTTP error! status: ${response.statusCode}`); - } - const imageBase64 = response.bodyAsBase64(); - const dir = dirname(savePath); - await ensureDirExists(dir); - - const imageName = JmImageTool.getFileNameFromUrl(imageUrl, false); - const num = JmImageTool.getNum(scrambleId, photoId, imageName); - await JmImageTool.decodeAndSave(num, imageBase64, savePath); - - return true; - } catch (error: any) { - console.error(`[图片] 下载失败: ${imageUrl}, 错误: ${error.message}`); - return false; - } - } - - private toUrlSearchParams(obj: Record): string { - return Object.keys(obj).map(k => `${encodeURIComponent(k)}=${encodeURIComponent(obj[k])}`).join('&'); - } - - private parseAlbumData(albumId: string, data: any): Album { - const episodeList = data.series && data.series.length > 0 ? data.series : [{ id: albumId, title: data.name }]; - return { - id: albumId, - title: data.name || `本子 ${albumId}`, - author: (data.author && data.author[0]) || '未知作者', - episodeList: episodeList, - scrambleId: data.scramble_id || JmMagicConstants.SCRAMBLE_220980, - length: episodeList.length, - }; - } - - private parsePhotoData(photoId: string, data: any): Photo { - return { - id: photoId, - title: data.name || `章节 ${photoId}`, - pageArr: data.images || [], - albumId: data.album_id || photoId, - scrambleId: data.scramble_id || JmMagicConstants.SCRAMBLE_220980, - length: (data.images || []).length, - }; - } - } - - class JmDownloaderImpl { - public option: JmOptionImpl; - public client: JmApiClientImpl; - - constructor(option: JmOptionImpl) { - this.option = option; - this.client = option.buildJmClient(); - } - - async downloadAlbum(albumId: string): Promise { - const album = await this.client.getAlbumDetail(albumId); - await this.downloadByAlbumDetail(album); - return album; - } - - private async downloadByAlbumDetail(album: Album): Promise { - const albumDir = this.option.dirRule.decideAlbumRootDir(album); - await ensureDirExists(albumDir); - console.log(`[专辑: ${album.title}] 发现 ${album.episodeList.length} 个章节, 开始下载...`); - - const chapterConcurrency = 5; - const tasks = album.episodeList.map((episode, i) => async () => { - console.log(` [章节 ${i + 1}/${album.episodeList.length}] 开始下载: ${episode.title} (${episode.id})`); - try { - const photo = await this.client.getPhotoDetail(episode.id); - await this.downloadPhotoImages(photo, albumDir, album.id); - console.log(` [章节 ${i + 1}/${album.episodeList.length}] 下载完成: ${episode.title}`); - } catch (e: any) { - console.error(` [章节 ${i + 1}/${album.episodeList.length}] 下载失败: ${episode.title}, 错误: ${e.message}`); - } - }); - - await runTasksWithConcurrency(tasks, chapterConcurrency); - } - - private async downloadPhotoImages(photo: Photo, albumDir: string, albumId: string): Promise { - if (!photo.pageArr || photo.pageArr.length === 0) return; - - console.log(` [图片集: ${photo.title}] 发现 ${photo.pageArr.length} 张图片, 开始下载...`); - const concurrencyLimit = 10; - - const tasks = photo.pageArr.map((imageName, i) => { - return async () => { - const finalFileName = `${(i + 1).toString().padStart(5, '0')}.jpg`; - const filePath = joinPath(albumDir, finalFileName); - - const fileExists = await Tools.Files.exists(filePath); - if (fileExists.exists) { - return; - } - - const imageUrl = this.buildImageUrl(photo, imageName); - try { - await this.client.downloadImage(imageUrl, filePath, photo.scrambleId, photo.id); - } catch (e: any) { - console.error(` [图片下载失败] ${finalFileName} from ${photo.title}: ${e.message}`); - } - }; - }); - - await runTasksWithConcurrency(tasks, concurrencyLimit); - console.log(` [图片集: ${photo.title}] 所有图片下载任务已处理。`); - } - - private getFileExtension(filename: string): string { - const dotIndex = filename.lastIndexOf('.'); - return dotIndex > 0 ? filename.substring(dotIndex + 1) : 'jpg'; - } - - private buildImageUrl(photo: Photo, imageName: string): string { - const domain = JmModuleConfig.DOMAIN_IMAGE_LIST[Math.floor(Math.random() * JmModuleConfig.DOMAIN_IMAGE_LIST.length)]; - return `${JmModuleConfig.PROT}${domain}/media/photos/${photo.albumId}/${imageName}`; - } - } - - class SimpleJMDownloader { - private downloader: JmDownloaderImpl; - private client: JmApiClientImpl; - private option: JmOptionImpl; - - constructor(downloadDir: string = DEFAULT_DOWNLOAD_DIR) { - this.option = JmOptionImpl.default(downloadDir); - this.downloader = new JmDownloaderImpl(this.option); - this.client = this.option.buildJmClient(); - console.log(`✅ JM下载器初始化成功, 下载目录: ${this.option.dirRule.baseDir}`); - } - - async searchComics(params: SearchParams): Promise { - console.log(`🔍 搜索漫画: ${params.query}`); - return await this.client.searchComics(params); - } - - async getAlbumInfo(albumId: string): Promise { - const album = await this.client.getAlbumDetail(albumId); - return { - id: album.id, - title: album.title, - author: album.author, - chapterCount: album.length, - success: true - }; - } - - async downloadAlbum(albumId: string): Promise { - console.log(`📖 获取本子信息: ${albumId}`); - try { - const info = await this.getAlbumInfo(albumId); - if (!info.success) return { success: false, albumId, error: "获取信息失败" }; - - console.log(`📥 开始下载本子: ${info.title}`); - await this.downloader.downloadAlbum(albumId); - const downloadedFiles = await this._checkDownloadedFiles(info.title); - - return { - success: true, - albumId: albumId, - title: info.title, - downloadedFiles - }; - } catch (error: any) { - return { success: false, albumId, error: error.message }; - } - } - - async batchDownload(albumIds: string[]): Promise { - const results: BatchDownloadResult[] = []; - console.log(`📦 开始批量下载 ${albumIds.length} 个本子`); - - const concurrencyLimit = 3; // 限制并发下载的漫画数量 - - const tasks = albumIds.map((albumId, i) => async (): Promise => { - console.log(`\n[${i + 1}/${albumIds.length}] 开始处理本子: ${albumId}`); - const result = await this.downloadAlbum(albumId); - if (result.success) { - console.log(`✅ [${i + 1}/${albumIds.length}] 下载成功: ${result.title}`); - } else { - console.log(`❌ [${i + 1}/${albumIds.length}] 下载失败: ${albumId}, ${result.error || 'Unknown error'}`); - } - return result; - }); - - return await runTasksWithConcurrency(tasks, concurrencyLimit); - } - - private async _checkDownloadedFiles(title: string): Promise> { - const albumDir = this.option.dirRule.decideAlbumRootDir({ title }); - const dirExists = await Tools.Files.exists(albumDir); - if (dirExists.exists) { - const listResult = await Tools.Files.list(albumDir); - const files = listResult.entries.map(e => e.name); - return { - directory: albumDir, - fileCount: files.length, - files: files.slice(0, 10) - }; - } - return { directory: undefined, fileCount: 0, files: [] }; - } - } - //endregion - - //region Tool Implementations - async function main() { - console.log("🚀 开始执行JMComic工具功能测试..."); - const downloader = new SimpleJMDownloader(TEST_DOWNLOAD_DIR); - const testQuery = "原神"; - - console.log(`1. 测试搜索功能,关键词: "${testQuery}"`); - const searchResult = await downloader.searchComics({ query: testQuery }); - - if (!searchResult || !searchResult.results || searchResult.results.length === 0) { - throw new Error(`搜索测试失败: 未能找到关于 "${testQuery}" 的任何结果。`); - } - - console.log(`✅ 搜索成功, 找到 ${searchResult.total_results} 个结果。`); - const firstAlbum = searchResult.results[0]; - console.log(`2. 测试获取作品信息功能, 作品ID: ${firstAlbum.id} (${firstAlbum.title})`); - - const albumInfo = await downloader.getAlbumInfo(firstAlbum.id); - - if (!albumInfo || !albumInfo.success) { - throw new Error(`获取作品信息失败, ID: ${firstAlbum.id}`); - } - - console.log(`✅ 作品信息获取成功:`); - console.log(` - 标题: ${albumInfo.title}`); - console.log(` - 作者: ${albumInfo.author}`); - console.log(` - 章节数: ${albumInfo.chapterCount}`); - - console.log(`3. 测试下载功能, 作品ID: ${firstAlbum.id} (${firstAlbum.title})`); - const downloadResult = await downloader.downloadAlbum(firstAlbum.id); - - if (!downloadResult || !downloadResult.success) { - throw new Error(`下载作品失败, ID: ${firstAlbum.id}`); - } - - console.log(`✅ 下载成功:`); - console.log(` - 保存目录: ${downloadResult.downloadedFiles!.directory}`); - console.log(` - 文件数量: ${downloadResult.downloadedFiles!.fileCount}`); - - const summary = `JMComic工具测试完成。成功搜索、获取信息并下载了作品《${albumInfo.title}》。`; - console.log(`\n${summary}`); - return summary; - } - - async function search_comics(params: SearchParams) { - const downloader = new SimpleJMDownloader(); - return await downloader.searchComics(params); - } - - async function get_album_info(params: { album_id: string }) { - const downloader = new SimpleJMDownloader(); - return await downloader.getAlbumInfo(params.album_id); - } - - async function download_album(params: { album_id: string, download_dir?: string }) { - const downloader = new SimpleJMDownloader(params.download_dir); - return await downloader.downloadAlbum(params.album_id); - } - - async function batch_download_albums(params: { album_ids: string, download_dir?: string }) { - const albumIds = params.album_ids.split(',').map(id => id.trim()).filter(id => id); - if (albumIds.length === 0) throw new Error("album_ids不能为空"); - const downloader = new SimpleJMDownloader(params.download_dir); - return await downloader.batchDownload(albumIds); - } - - async function jmcomic_wrap(func: Function, params: any, successMessage: string, failMessage: string) { - try { - console.log(`开始执行: ${func.name}`); - const result = await func(params); - complete({ success: true, message: successMessage, data: result }); - } catch (error: any) { - console.error(`${func.name} 执行失败: ${error.message}`); - complete({ success: false, message: `${failMessage}: ${error.message}`, error_stack: error.stack }); - } - } - //endregion - - return { - main: (p: any) => jmcomic_wrap(main, p, '功能测试完成', '功能测试失败'), - search_comics: (p: SearchParams) => jmcomic_wrap(search_comics, p, '搜索完成', '搜索失败'), - get_album_info: (p: { album_id: string }) => jmcomic_wrap(get_album_info, p, '信息获取完成', '信息获取失败'), - download_album: (p: { album_id: string, download_dir?: string }) => jmcomic_wrap(download_album, p, '下载完成', '下载失败'), - batch_download_albums: (p: { album_ids: string, download_dir?: string }) => jmcomic_wrap(batch_download_albums, p, '批量下载完成', '批量下载失败'), - }; -})(); - -exports.main = jmcomic.main; -exports.search_comics = jmcomic.search_comics; -exports.get_album_info = jmcomic.get_album_info; -exports.download_album = jmcomic.download_album; -exports.batch_download_albums = jmcomic.batch_download_albums; diff --git a/scripts/minimax_draw.ts b/scripts/minimax_draw.ts deleted file mode 100644 index acae82a..0000000 --- a/scripts/minimax_draw.ts +++ /dev/null @@ -1,691 +0,0 @@ -/* METADATA -{ - "name": "minimax_draw", - "display_name": { - "zh": "MiniMax 绘图", - "en": "MiniMax Draw" - }, - "description": { - "zh": "使用 MiniMax 官方图像生成接口 (/v1/image_generation) 生成图片,支持文生图和带参考图的生图;结果保存到本地 /sdcard/Download/Operit/plugins/draw/minimax_draw/draws/ 目录,并返回 Markdown 图片提示。", - "en": "Generate images with the official MiniMax image generation API (/v1/image_generation). Supports text-to-image and reference-image generation. Saves results to /sdcard/Download/Operit/plugins/draw/minimax_draw/draws/ and returns Markdown image hints." - }, - "category": "Draw", - "env": [ - { - "name": "MINIMAX_API_KEY", - "description": { - "zh": "MiniMax API Key(必填)", - "en": "MiniMax API key (required)" - }, - "required": true - }, - { - "name": "MINIMAX_API_BASE_URL", - "description": { - "zh": "MiniMax API Base URL(可选;默认 https://api.minimaxi.com,国际站可改为 https://api.minimax.io)", - "en": "MiniMax API base URL (optional; defaults to https://api.minimaxi.com, international users can use https://api.minimax.io)" - }, - "required": false - }, - { - "name": "MINIMAX_IMAGE_MODEL", - "description": { - "zh": "默认图片模型(可选;未传 model 时使用,默认 image-01)", - "en": "Default image model (optional; used when model is omitted, default image-01)" - }, - "required": false - }, - { - "name": "BEEIMG_API_KEY", - "description": { - "zh": "BeeIMG API Key(可选;仅当 image_paths 传本地参考图路径时需要,用于先上传到公网图床)", - "en": "BeeIMG API key (optional; only needed when image_paths is used so local reference images can be uploaded first)" - }, - "required": false - } - ], - "tools": [ - { - "name": "draw_image", - "description": { - "zh": "根据提示词调用 MiniMax 官方绘图接口生成图片,支持文生图和参考图生图,保存到本地并返回 Markdown 图片提示。", - "en": "Generate images with the official MiniMax image API using a prompt. Supports text-to-image and reference-image generation, saves locally, and returns Markdown image hints." - }, - "parameters": [ - { "name": "prompt", "description": { "zh": "绘图提示词(中文或英文)", "en": "Image prompt (Chinese or English)" }, "type": "string", "required": true }, - { "name": "model", "description": { "zh": "模型名称(可选;不传则使用环境变量 MINIMAX_IMAGE_MODEL,否则默认 image-01)", "en": "Model name (optional; falls back to MINIMAX_IMAGE_MODEL, then image-01)" }, "type": "string", "required": false }, - { "name": "aspect_ratio", "description": { "zh": "输出比例,可选 1:1、16:9、4:3、3:2、2:3、3:4、9:16、21:9;21:9 仅 image-01 支持", "en": "Aspect ratio. Supported: 1:1, 16:9, 4:3, 3:2, 2:3, 3:4, 9:16, 21:9. 21:9 is only supported by image-01." }, "type": "string", "required": false }, - { "name": "width", "description": { "zh": "输出宽度(可选;仅 image-01 支持;需与 height 一起传,范围 512-2048 且为 8 的倍数)", "en": "Output width (optional; image-01 only; must be provided together with height, range 512-2048 and multiple of 8)" }, "type": "number", "required": false }, - { "name": "height", "description": { "zh": "输出高度(可选;仅 image-01 支持;需与 width 一起传,范围 512-2048 且为 8 的倍数)", "en": "Output height (optional; image-01 only; must be provided together with width, range 512-2048 and multiple of 8)" }, "type": "number", "required": false }, - { "name": "n", "description": { "zh": "生成图片数量,范围 1-9,默认 1", "en": "Number of images to generate, range 1-9, default 1" }, "type": "number", "required": false }, - { "name": "response_format", "description": { "zh": "返回格式,可选 url 或 base64;默认 base64(更适合直接保存到本地)", "en": "Response format: url or base64. Defaults to base64 for direct local saving." }, "type": "string", "required": false }, - { "name": "prompt_optimizer", "description": { "zh": "是否启用提示词优化(可选)", "en": "Enable prompt optimizer (optional)" }, "type": "boolean", "required": false }, - { "name": "aigc_watermark", "description": { "zh": "是否添加 AIGC 水印(可选)", "en": "Add AIGC watermark (optional)" }, "type": "boolean", "required": false }, - { "name": "seed", "description": { "zh": "随机种子(可选)", "en": "Seed (optional)" }, "type": "number", "required": false }, - { "name": "image_urls", "description": { "zh": "参考图公网 URL 数组(可选;图生图用)。支持字符串数组、JSON 字符串或逗号分隔字符串", "en": "Reference image public URL list (optional; for reference-image generation). Accepts string array, JSON string, or comma-separated string." }, "type": "array", "required": false }, - { "name": "image_paths", "description": { "zh": "参考图本地路径数组(可选;会先上传到图床再提交给 MiniMax)。支持字符串数组、JSON 字符串或逗号分隔字符串", "en": "Reference local image path list (optional; local images are uploaded first before calling MiniMax). Accepts string array, JSON string, or comma-separated string." }, "type": "array", "required": false }, - { "name": "file_name", "description": { "zh": "自定义本地文件名(不含扩展名)", "en": "Custom local output file name without extension" }, "type": "string", "required": false }, - { "name": "api_base_url", "description": { "zh": "自定义 MiniMax API Base URL(可选)", "en": "Custom MiniMax API base URL (optional)" }, "type": "string", "required": false } - ] - } - ] -}*/ - -/// - -const minimaxDraw = (function () { - const HTTP_TIMEOUT_MS = 600000; - const client = OkHttp.newBuilder() - .connectTimeout(HTTP_TIMEOUT_MS) - .readTimeout(HTTP_TIMEOUT_MS) - .writeTimeout(HTTP_TIMEOUT_MS) - .build(); - - const DEFAULT_API_BASE_URL = "https://api.minimaxi.com"; - const DEFAULT_MODEL = "image-01"; - const DEFAULT_RESPONSE_FORMAT = "base64"; - const DEFAULT_IMAGE_COUNT = 1; - const DEFAULT_BASE64_EXTENSION = "jpeg"; - const SUPPORTED_ASPECT_RATIOS = ["1:1", "16:9", "4:3", "3:2", "2:3", "3:4", "9:16", "21:9"]; - const BEEIMG_UPLOAD_ENDPOINT = "https://beeimg.com/api/upload/file/json/"; - - const DRAW_ROOT_DIR = getPluginConfigDir("draw"); - const STORAGE_DIR = `${DRAW_ROOT_DIR}/minimax_draw`; - const DRAWS_DIR = `${STORAGE_DIR}/draws`; - - function isRecord(value) { - return typeof value === "object" && value !== null && !Array.isArray(value); - } - - function getErrorMessage(error) { - if (error instanceof Error) return error.message; - return String(error); - } - - function getApiKey() { - const apiKey = String(getEnv("MINIMAX_API_KEY") || "").trim(); - if (!apiKey) { - throw new Error("MINIMAX_API_KEY 未配置,请先在环境变量中设置 MiniMax API Key。"); - } - return apiKey; - } - - function joinUrl(baseUrl, path) { - const normalizedBase = String(baseUrl || "").endsWith("/") ? String(baseUrl || "") : `${baseUrl}/`; - const normalizedPath = String(path || "").startsWith("/") ? String(path).slice(1) : String(path || ""); - return `${normalizedBase}${normalizedPath}`; - } - - function getApiBaseUrl(customBaseUrl) { - const fromParam = String(customBaseUrl || "").trim(); - if (fromParam) return fromParam; - - const fromEnv = String(getEnv("MINIMAX_API_BASE_URL") || "").trim(); - if (fromEnv) return fromEnv; - - return DEFAULT_API_BASE_URL; - } - - function getImageEndpoint(baseUrl) { - const trimmed = String(baseUrl || "").trim(); - if (!trimmed) return joinUrl(DEFAULT_API_BASE_URL, "v1/image_generation"); - if (trimmed.includes("/v1/image_generation")) return trimmed; - if (trimmed.endsWith("/v1")) return joinUrl(trimmed, "image_generation"); - if (trimmed.endsWith("/v1/")) return joinUrl(trimmed, "image_generation"); - return joinUrl(trimmed, "v1/image_generation"); - } - - function resolveModel(model) { - const fromParam = String(model || "").trim(); - if (fromParam) return fromParam; - - const fromEnv = String(getEnv("MINIMAX_IMAGE_MODEL") || "").trim(); - if (fromEnv) return fromEnv; - - return DEFAULT_MODEL; - } - - function sanitizeFileName(name, fallbackPrefix) { - const safe = String(name || "").replace(/[\\/:*?"<>|]/g, "_").trim(); - if (!safe) return `${fallbackPrefix}_${Date.now()}`; - return safe.substring(0, 80); - } - - function buildFileBaseName(prompt, customName, fallbackPrefix) { - const fromCustom = String(customName || "").trim(); - if (fromCustom) { - return sanitizeFileName(fromCustom, fallbackPrefix); - } - const rawPrompt = String(prompt || "").trim(); - const shortPrompt = rawPrompt.length > 40 ? `${rawPrompt.substring(0, 40)}...` : rawPrompt; - return `${sanitizeFileName(shortPrompt || fallbackPrefix, fallbackPrefix)}_${Date.now()}`; - } - - async function ensureDirectories() { - const dirs = [DRAW_ROOT_DIR, STORAGE_DIR, DRAWS_DIR]; - for (const dir of dirs) { - try { - const result = await Tools.Files.mkdir(dir); - if (!result.successful) { - console.warn(`创建目录失败(可能已存在): ${dir} -> ${result.details}`); - } - } catch (error) { - console.warn(`创建目录异常: ${dir} -> ${getErrorMessage(error)}`); - } - } - } - - function parsePositiveInteger(value, fieldName, options?) { - const hasDefault = !!options && Object.prototype.hasOwnProperty.call(options, "defaultValue"); - const defaultValue = hasDefault ? options.defaultValue : undefined; - - if (value === undefined || value === null || value === "") { - return defaultValue; - } - - const numberValue = typeof value === "number" ? value : Number(String(value)); - if (!Number.isFinite(numberValue) || numberValue <= 0 || !Number.isInteger(numberValue)) { - throw new Error(`${fieldName} 必须是正整数。`); - } - - if (options && options.min !== undefined && numberValue < options.min) { - throw new Error(`${fieldName} 不能小于 ${options.min}。`); - } - - if (options && options.max !== undefined && numberValue > options.max) { - throw new Error(`${fieldName} 不能大于 ${options.max}。`); - } - - return numberValue; - } - - function parseInteger(value, fieldName) { - if (value === undefined || value === null || value === "") { - return undefined; - } - - const numberValue = typeof value === "number" ? value : Number(String(value)); - if (!Number.isFinite(numberValue) || !Number.isInteger(numberValue)) { - throw new Error(`${fieldName} 必须是整数。`); - } - - return numberValue; - } - - function parseBoolean(value, fieldName) { - if (value === undefined || value === null || value === "") { - return undefined; - } - - if (typeof value === "boolean") { - return value; - } - - const normalized = String(value).trim().toLowerCase(); - if (normalized === "true" || normalized === "1" || normalized === "yes") { - return true; - } - if (normalized === "false" || normalized === "0" || normalized === "no") { - return false; - } - - throw new Error(`${fieldName} 必须是布尔值。`); - } - - function normalizeResponseFormat(value) { - const normalized = String(value || DEFAULT_RESPONSE_FORMAT).trim().toLowerCase(); - if (normalized !== "url" && normalized !== "base64") { - throw new Error("response_format 仅支持 url 或 base64。"); - } - return normalized; - } - - function normalizeAspectRatio(model, value) { - const normalized = String(value || "").trim(); - if (!normalized) return undefined; - - if (!SUPPORTED_ASPECT_RATIOS.includes(normalized)) { - throw new Error(`aspect_ratio 仅支持 ${SUPPORTED_ASPECT_RATIOS.join("、")}。`); - } - - if (normalized === "21:9" && String(model || "").trim() !== "image-01") { - throw new Error("aspect_ratio=21:9 仅支持 model=image-01。"); - } - - return normalized; - } - - function normalizeDimensions(model, widthValue, heightValue, aspectRatio) { - if (aspectRatio) { - return { - width: undefined, - height: undefined - }; - } - - const width = parsePositiveInteger(widthValue, "width"); - const height = parsePositiveInteger(heightValue, "height"); - - if (width === undefined && height === undefined) { - return { - width: undefined, - height: undefined - }; - } - - if (width === undefined || height === undefined) { - throw new Error("width 和 height 需要同时传入。"); - } - - if (String(model || "").trim() !== "image-01") { - throw new Error("width 和 height 仅支持 model=image-01。"); - } - - if (width < 512 || width > 2048 || width % 8 !== 0) { - throw new Error("width 需要在 512-2048 之间,且必须为 8 的倍数。"); - } - - if (height < 512 || height > 2048 || height % 8 !== 0) { - throw new Error("height 需要在 512-2048 之间,且必须为 8 的倍数。"); - } - - return { width, height }; - } - - function parseStringList(value, fieldName) { - if (value === undefined || value === null || value === "") { - return []; - } - - if (Array.isArray(value)) { - return value - .map(item => String(item || "").trim()) - .filter(item => item.length > 0); - } - - if (typeof value === "string") { - const trimmed = value.trim(); - if (!trimmed) return []; - - try { - const parsed = JSON.parse(trimmed); - if (Array.isArray(parsed)) { - return parsed - .map(item => String(item || "").trim()) - .filter(item => item.length > 0); - } - } catch { - // ignore and try comma-separated parsing - } - - return trimmed - .split(",") - .map(item => item.trim()) - .filter(item => item.length > 0); - } - - throw new Error(`${fieldName} 必须是字符串数组、JSON 字符串或逗号分隔字符串。`); - } - - function isProbablyUrl(value) { - return /^https?:\/\//i.test(String(value || "").trim()); - } - - function getBeeimgApiKey() { - return String(getEnv("BEEIMG_API_KEY") || "").trim(); - } - - function guessMimeTypeFromPath(filePath) { - const lower = String(filePath || "").toLowerCase(); - if (lower.endsWith(".png")) return "image/png"; - if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg"; - if (lower.endsWith(".webp")) return "image/webp"; - if (lower.endsWith(".gif")) return "image/gif"; - return "application/octet-stream"; - } - - async function uploadImageToBeeimg(filePath) { - const exists = await Tools.Files.exists(filePath); - if (!exists.exists) { - throw new Error(`参考图文件不存在: ${filePath}`); - } - - const apiKey = getBeeimgApiKey(); - if (!apiKey) { - throw new Error("使用 image_paths 需要配置 BEEIMG_API_KEY,用于先把本地参考图上传到公网图床。"); - } - - const response = await Tools.Net.uploadFile({ - url: BEEIMG_UPLOAD_ENDPOINT, - method: "POST", - form_data: { - apikey: apiKey - }, - files: [ - { - field_name: "file", - file_path: filePath, - content_type: guessMimeTypeFromPath(filePath) - } - ] - }); - - if (response.statusCode < 200 || response.statusCode >= 300) { - throw new Error(`BeeIMG 上传失败: HTTP ${response.statusCode} - ${response.content}`); - } - - let parsed; - try { - parsed = JSON.parse(response.content); - } catch (error) { - throw new Error(`解析 BeeIMG 响应失败: ${getErrorMessage(error)}`); - } - - const files = isRecord(parsed) && isRecord(parsed.files) ? parsed.files : null; - const fileUrl = files && files.url ? String(files.url).trim() : ""; - const code = files && files.code !== undefined ? String(files.code) : ""; - const status = files && files.status ? String(files.status) : ""; - - if (!fileUrl || (code !== "200" && code !== "" && status !== "Success")) { - throw new Error(`BeeIMG 上传失败: ${response.content}`); - } - - return fileUrl; - } - - async function resolveSubjectReference(imageUrls, imagePaths) { - const resolvedUrls = parseStringList(imageUrls, "image_urls"); - const resolvedPaths = parseStringList(imagePaths, "image_paths"); - - for (const url of resolvedUrls) { - if (!isProbablyUrl(url)) { - throw new Error(`image_urls 中包含无效链接: ${url}`); - } - } - - for (const filePath of resolvedPaths) { - const uploadedUrl = await uploadImageToBeeimg(filePath); - resolvedUrls.push(uploadedUrl); - } - - if (resolvedUrls.length === 0) { - return undefined; - } - - return resolvedUrls.map(url => ({ - type: "character", - image_file: url - })); - } - - function normalizeBase64(base64) { - const raw = String(base64 || "").trim(); - if (!raw) return raw; - - const prefixIndex = raw.indexOf("base64,"); - if (raw.startsWith("data:") && prefixIndex >= 0) { - return raw.substring(prefixIndex + "base64,".length).trim(); - } - - return raw; - } - - function guessExtensionFromUrl(url, fallbackExtension) { - const match = String(url || "").match(/\.([a-z0-9]{2,5})(?:[?#].*)?$/i); - if (match && match[1]) return match[1].toLowerCase(); - return fallbackExtension; - } - - function parseMiniMaxError(parsed, fallbackMessage) { - const baseResp = isRecord(parsed) && isRecord(parsed.base_resp) ? parsed.base_resp : null; - if (!baseResp) return; - - const statusCode = baseResp.status_code !== undefined ? Number(baseResp.status_code) : 0; - if (Number.isFinite(statusCode) && statusCode !== 0) { - const statusMsg = baseResp.status_msg ? String(baseResp.status_msg) : fallbackMessage; - throw new Error(`MiniMax 图片接口返回错误: ${statusMsg} (status_code=${statusCode})`); - } - } - - async function callMiniMaxImageApi(params) { - const apiBaseUrl = getApiBaseUrl(params.api_base_url); - const endpoint = getImageEndpoint(apiBaseUrl); - const effectiveModel = resolveModel(params.model); - const responseFormat = normalizeResponseFormat(params.response_format); - const aspectRatio = normalizeAspectRatio(effectiveModel, params.aspect_ratio); - const dimensions = normalizeDimensions(effectiveModel, params.width, params.height, aspectRatio); - const imageCount = parsePositiveInteger(params.n, "n", { defaultValue: DEFAULT_IMAGE_COUNT, min: 1, max: 9 }); - const promptOptimizer = parseBoolean(params.prompt_optimizer, "prompt_optimizer"); - const aigcWatermark = parseBoolean(params.aigc_watermark, "aigc_watermark"); - const seed = parseInteger(params.seed, "seed"); - const subjectReference = await resolveSubjectReference(params.image_urls, params.image_paths); - - const body: Record = { - model: effectiveModel, - prompt: String(params.prompt || "").trim(), - response_format: responseFormat, - n: imageCount - }; - - if (aspectRatio) { - body.aspect_ratio = aspectRatio; - } - - if (dimensions.width !== undefined && dimensions.height !== undefined) { - body.width = dimensions.width; - body.height = dimensions.height; - } - - if (promptOptimizer !== undefined) { - body.prompt_optimizer = promptOptimizer; - } - - if (aigcWatermark !== undefined) { - body.aigc_watermark = aigcWatermark; - } - - if (seed !== undefined) { - body.seed = seed; - } - - if (subjectReference) { - body.subject_reference = subjectReference; - } - - const request = client - .newRequest() - .url(endpoint) - .method("POST") - .headers({ - "accept": "application/json", - "content-type": "application/json", - "Authorization": `Bearer ${getApiKey()}` - }) - .body(JSON.stringify(body), "json"); - - const response = await request.build().execute(); - if (!response.isSuccessful()) { - throw new Error(`MiniMax 图片 API 调用失败: ${response.statusCode} - ${response.content}`); - } - - let parsed; - try { - parsed = JSON.parse(response.content); - } catch (error) { - throw new Error(`解析 MiniMax 响应失败: ${getErrorMessage(error)}`); - } - - parseMiniMaxError(parsed, "MiniMax 接口返回错误"); - - const data = isRecord(parsed) && isRecord(parsed.data) ? parsed.data : null; - if (!data) { - throw new Error("MiniMax 响应中未找到 data 对象。"); - } - - const metadata = isRecord(data.metadata) ? data.metadata : null; - const successCount = metadata && metadata.success_count !== undefined ? Number(metadata.success_count) : undefined; - const failedCount = metadata && metadata.failed_count !== undefined ? Number(metadata.failed_count) : undefined; - const requestId = isRecord(parsed) && parsed.id ? String(parsed.id) : ""; - - if (responseFormat === "base64") { - const imageBase64 = Array.isArray(data.image_base64) - ? data.image_base64.map(item => String(item || "").trim()).filter(item => item.length > 0) - : []; - - if (imageBase64.length === 0) { - throw new Error("MiniMax 响应中未找到 data.image_base64。"); - } - - return { - request_id: requestId, - effective_model: effectiveModel, - response_format: responseFormat, - image_base64: imageBase64, - image_urls: [], - success_count: Number.isFinite(successCount) ? successCount : imageBase64.length, - failed_count: Number.isFinite(failedCount) ? failedCount : 0, - reference_count: subjectReference ? subjectReference.length : 0 - }; - } - - const imageUrls = Array.isArray(data.image_urls) - ? data.image_urls.map(item => String(item || "").trim()).filter(item => item.length > 0) - : []; - - if (imageUrls.length === 0) { - throw new Error("MiniMax 响应中未找到 data.image_urls。"); - } - - return { - request_id: requestId, - effective_model: effectiveModel, - response_format: responseFormat, - image_base64: [], - image_urls: imageUrls, - success_count: Number.isFinite(successCount) ? successCount : imageUrls.length, - failed_count: Number.isFinite(failedCount) ? failedCount : 0, - reference_count: subjectReference ? subjectReference.length : 0 - }; - } - - async function draw_image(params) { - const prompt = String(params && params.prompt ? params.prompt : "").trim(); - if (!prompt) { - throw new Error("参数 prompt 不能为空。"); - } - - await ensureDirectories(); - - const apiResult = await callMiniMaxImageApi({ - prompt, - model: params.model, - aspect_ratio: params.aspect_ratio, - width: params.width, - height: params.height, - n: params.n, - response_format: params.response_format, - prompt_optimizer: params.prompt_optimizer, - aigc_watermark: params.aigc_watermark, - seed: params.seed, - image_urls: params.image_urls, - image_paths: params.image_paths, - api_base_url: params.api_base_url - }); - - const baseName = buildFileBaseName(prompt, params.file_name, "minimax_draw"); - const files: Array<{ - file_path: string; - file_uri: string; - markdown: string; - remote_image_url: string | null; - }> = []; - - if (apiResult.response_format === "base64") { - for (let index = 0; index < apiResult.image_base64.length; index += 1) { - const suffix = apiResult.image_base64.length > 1 ? `_${index + 1}` : ""; - const filePath = `${DRAWS_DIR}/${baseName}${suffix}.${DEFAULT_BASE64_EXTENSION}`; - const writeResult = await Tools.Files.writeBinary(filePath, normalizeBase64(apiResult.image_base64[index])); - if (!writeResult.successful) { - throw new Error(`保存图片失败: ${writeResult.details}`); - } - const fileUri = `file://${filePath}`; - files.push({ - file_path: filePath, - file_uri: fileUri, - markdown: `![MiniMax 生成的图片${apiResult.image_base64.length > 1 ? ` ${index + 1}` : ""}](${fileUri})`, - remote_image_url: null - }); - } - } else { - for (let index = 0; index < apiResult.image_urls.length; index += 1) { - const imageUrl = apiResult.image_urls[index]; - const extension = guessExtensionFromUrl(imageUrl, "png"); - const suffix = apiResult.image_urls.length > 1 ? `_${index + 1}` : ""; - const filePath = `${DRAWS_DIR}/${baseName}${suffix}.${extension}`; - const downloadResult = await Tools.Files.download(imageUrl, filePath); - if (!downloadResult.successful) { - throw new Error(`下载图片失败: ${downloadResult.details}`); - } - const fileUri = `file://${filePath}`; - files.push({ - file_path: filePath, - file_uri: fileUri, - markdown: `![MiniMax 生成的图片${apiResult.image_urls.length > 1 ? ` ${index + 1}` : ""}](${fileUri})`, - remote_image_url: imageUrl - }); - } - } - - const hintLines: string[] = []; - hintLines.push(`图片已生成并保存在本地 ${DRAWS_DIR}。`); - hintLines.push(`共生成 ${files.length} 张。`); - if (apiResult.reference_count > 0) { - hintLines.push(`本次使用了 ${apiResult.reference_count} 张参考图。`); - } - hintLines.push(""); - hintLines.push("后续回答如果需要展示图片,请直接输出下面这些 Markdown:"); - hintLines.push(""); - for (const fileItem of files) { - hintLines.push(fileItem.markdown); - } - - return { - prompt, - model: apiResult.effective_model, - request_id: apiResult.request_id || null, - response_format: apiResult.response_format, - success_count: apiResult.success_count, - failed_count: apiResult.failed_count, - reference_count: apiResult.reference_count, - file_path: files[0].file_path, - file_uri: files[0].file_uri, - markdown: files[0].markdown, - files, - hint: hintLines.join("\n") - }; - } - - async function draw_image_wrapper(params) { - try { - const result = await draw_image(params || {}); - complete({ - success: true, - message: "MiniMax 图片生成成功,已保存到本地。", - data: result - }); - } catch (error) { - console.error("draw_image 执行失败:", error); - complete({ - success: false, - message: `MiniMax 图片生成失败: ${getErrorMessage(error)}`, - error_stack: error && error.stack ? error.stack : undefined - }); - } - } - - return { - draw_image: draw_image_wrapper - }; -})(); - -exports.draw_image = minimaxDraw.draw_image; diff --git a/scripts/nanobanana_draw.ts b/scripts/nanobanana_draw.ts deleted file mode 100644 index 0b7c814..0000000 --- a/scripts/nanobanana_draw.ts +++ /dev/null @@ -1,744 +0,0 @@ -/* METADATA -{ - "name": "nanobanana_draw", - - "display_name": { - "zh": "Nanobanana 绘图", - "en": "Nanobanana Draw" - }, - "description": { - "zh": "使用 Nano Banana API (基于Grsai的api服务/https://grsai.com/) 根据提示词画图,支持文生图和图生图(可传入参考图片URL或本地图片路径;本地图片会先上传到图床以获得公网URL),将图片保存到本地 /sdcard/Download/Operit/plugins/draw/nanobanana_draw/draws/ 目录,并返回 Markdown 图片提示。", - "en": "Generate images using the Nano Banana API (via Grsai service / https://grsai.com/). Supports text-to-image and image-to-image (you can provide reference image URLs or local image paths; local images will be uploaded first to get public URLs). Saves images to /sdcard/Download/Operit/plugins/draw/nanobanana_draw/draws/ and returns a Markdown image reference." - }, - "env": [ - "NANOBANANA_API_KEY", - "NANOBANANA_API_BASE_URL", - "BEEIMG_API_KEY" - ], - "category": "Draw", - "tools": [ - { - "name": "draw_image", - "description": { - "zh": "根据提示词调用 Nano Banana API 生成图片(支持文生图和图生图),保存到本地并返回 Markdown 图片提示。", - "en": "Generate an image via the Nano Banana API using a prompt (supports text-to-image and image-to-image), save locally, and return a Markdown image reference." - }, - "parameters": [ - { "name": "prompt", "description": { "zh": "绘图提示词(英文或中文皆可)", "en": "Image prompt (Chinese or English)" }, "type": "string", "required": true }, - { "name": "model", "description": { "zh": "Nano Banana 模型名称(优先级最高)。可填 nano-banana-pro(约1800积分)或 nano-banana(约400积分)", "en": "Nano Banana model name (highest priority). e.g. nano-banana-pro (~1800 credits) or nano-banana (~400 credits)." }, "type": "string", "required": false }, - { "name": "model_variant", "description": { "zh": "模型档位(二选一,可选):pro(约1800积分)或 nano(约400积分)。未传时默认 pro", "en": "Model tier (optional): pro (~1800 credits) or nano (~400 credits). Defaults to pro." }, "type": "string", "required": false }, - { "name": "aspect_ratio", "description": { "zh": "输出图像比例,如 '1:1', '16:9', 'auto' 等,可选", "en": "Output aspect ratio, e.g. '1:1', '16:9', 'auto' (optional)" }, "type": "string", "required": false }, - { "name": "image_size", "description": { "zh": "输出图像大小,仅 nano-banana-pro 支持,如 '1K', '2K', '4K',可选", "en": "Output image size (only supported by nano-banana-pro), e.g. '1K', '2K', '4K' (optional)" }, "type": "string", "required": false }, - { "name": "image_urls", "description": { "zh": "参考图URL数组(图生图),支持格式:字符串数组['https://...'] 或 JSON字符串'[\"https://...\"]' 或逗号分隔'url1,url2',可选", "en": "Reference image URL list for img2img. Accepts: string array ['https://...'], or JSON string '[\"https://...\"]', or comma-separated 'url1,url2' (optional)." }, "type": "array", "required": false }, - { "name": "image_paths", "description": { "zh": "参考图本地路径数组(图生图,会先上传图床再进行生成),支持格式:字符串数组['/sdcard/...'] 或 JSON字符串 或 逗号分隔,可选", "en": "Reference local image path list for img2img (will be uploaded first). Accepts: string array ['/sdcard/...'], or JSON string, or comma-separated list (optional)." }, "type": "array", "required": false }, - { "name": "file_name", "description": { "zh": "自定义保存到本地的文件名(不含路径和扩展名)", "en": "Custom output file name (without path or extension)" }, "type": "string", "required": false }, - { "name": "poll_interval_ms", "description": { "zh": "轮询间隔(毫秒),默认 5000", "en": "Polling interval (milliseconds), default 5000" }, "type": "number", "required": false }, - { "name": "max_wait_time_ms", "description": { "zh": "最长等待时间(毫秒)。默认 10 分钟", "en": "Max wait time (milliseconds). Default 10 minutes." }, "type": "number", "required": false } - ] - } - ] -}*/ - -const nanobananaDraw = (function () { - const HTTP_TIMEOUT_MS = 600000; - const client = OkHttp.newBuilder() - .connectTimeout(HTTP_TIMEOUT_MS) - .readTimeout(HTTP_TIMEOUT_MS) - .writeTimeout(HTTP_TIMEOUT_MS) - .build(); - - const BEEIMG_UPLOAD_ENDPOINT = "https://beeimg.com/api/upload/file/json/"; - - // API配置 - const DEFAULT_API_BASE_URL = "https://grsai.dakka.com.cn"; - const DRAW_API_PATH = "v1/draw/nano-banana"; - const RESULT_API_PATH = "v1/draw/result"; - const MODEL_PRO = "nano-banana-pro"; - const MODEL_NANO = "nano-banana"; - const DEFAULT_MODEL = MODEL_PRO; - // Android 实际路径为 /sdcard/Download,对应系统中文名"下载" - const DRAW_ROOT_DIR = getPluginConfigDir("draw"); - const STORAGE_DIR = `${DRAW_ROOT_DIR}/nanobanana_draw`; - const DRAWS_DIR = `${STORAGE_DIR}/draws`; - - // 轮询配置 - const POLL_INTERVAL = 5000; // 每5秒查询一次 - const MAX_WAIT_TIME = 600000; // 最多等待10分钟 - - function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null; - } - - function getErrorMessage(error: unknown): string { - if (error instanceof Error) return error.message; - return String(error); - } - - function getErrorStack(error: unknown): string | undefined { - if (error instanceof Error) return error.stack; - return undefined; - } - - function resolveModel(model?: string, modelVariant?: string): string { - if (model && model.trim().length > 0) { - return model.trim(); - } - - if (!modelVariant || modelVariant.trim().length === 0) { - return DEFAULT_MODEL; - } - - const normalizedVariant = modelVariant.trim().toLowerCase(); - if (normalizedVariant === "pro") { - return MODEL_PRO; - } - if (normalizedVariant === "nano") { - return MODEL_NANO; - } - - throw new Error("参数 model_variant 仅支持 'pro' 或 'nano'。"); - } - - function normalizePositiveInt(value: unknown, fallback: number): number { - if (value === undefined || value === null) { - return fallback; - } - const n = typeof value === "number" ? value : parseInt(String(value), 10); - if (!Number.isFinite(n) || n <= 0) { - return fallback; - } - return Math.floor(n); - } - - function getApiKey(): string { - const apiKey = getEnv("NANOBANANA_API_KEY"); - if (!apiKey) { - throw new Error("NANOBANANA_API_KEY 未配置,请在环境变量中设置 Nano Banana 的 API Key。"); - } - return apiKey; - } - - function getBeeimgApiKey(): string { - return getEnv("BEEIMG_API_KEY") || ""; - } - - function joinUrl(baseUrl: string, path: string): string { - const normalizedBase = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`; - const normalizedPath = path.startsWith("/") ? path.slice(1) : path; - return `${normalizedBase}${normalizedPath}`; - } - - function getApiBaseUrl(): string { - const fromEnv = (getEnv("NANOBANANA_API_BASE_URL") || "").trim(); - if (fromEnv) return fromEnv; - return DEFAULT_API_BASE_URL; - } - - function getDrawEndpoint(baseUrl: string): string { - const trimmed = baseUrl.trim(); - if (!trimmed) return joinUrl(DEFAULT_API_BASE_URL, DRAW_API_PATH); - if (trimmed.includes("/v1/draw/nano-banana")) return trimmed; - if (trimmed.endsWith("/v1")) return joinUrl(trimmed, "draw/nano-banana"); - if (trimmed.endsWith("/v1/")) return joinUrl(trimmed, "draw/nano-banana"); - return joinUrl(trimmed, DRAW_API_PATH); - } - - function getResultEndpoint(baseUrl: string): string { - const trimmed = baseUrl.trim(); - if (!trimmed) return joinUrl(DEFAULT_API_BASE_URL, RESULT_API_PATH); - if (trimmed.includes("/v1/draw/result")) return trimmed; - if (trimmed.includes("/v1/draw/nano-banana")) { - return trimmed.replace("/v1/draw/nano-banana", "/v1/draw/result"); - } - if (trimmed.endsWith("/v1")) return joinUrl(trimmed, "draw/result"); - if (trimmed.endsWith("/v1/")) return joinUrl(trimmed, "draw/result"); - return joinUrl(trimmed, RESULT_API_PATH); - } - - function guessMimeTypeFromPath(filePath: string): string { - const lower = filePath.toLowerCase(); - if (lower.endsWith(".png")) return "image/png"; - if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg"; - if (lower.endsWith(".webp")) return "image/webp"; - if (lower.endsWith(".gif")) return "image/gif"; - return "application/octet-stream"; - } - - function safeJsonParseLoose(text: string): unknown { - const trimmed = (text || "").trim(); - if (!trimmed) return null; - try { - return JSON.parse(trimmed) as unknown; - } catch (e) { - const start = trimmed.indexOf("{"); - const end = trimmed.lastIndexOf("}"); - if (start !== -1 && end !== -1 && end > start) { - return JSON.parse(trimmed.substring(start, end + 1)) as unknown; - } - throw e; - } - } - - function isApiSuccessResponse(parsed: Record): boolean { - const code = parsed["code"]; - return code === undefined || code === null || code === 0 || code === "0"; - } - - function extractTaskPayload(parsed: unknown): Record | null { - if (!isRecord(parsed)) { - return null; - } - if (isRecord(parsed["data"])) { - return parsed["data"] as Record; - } - return parsed; - } - - function extractMessage(parsed: Record, fallback: string): string { - const payload = extractTaskPayload(parsed); - const candidates: unknown[] = [ - parsed["msg"], - parsed["message"], - payload ? payload["failure_reason"] : undefined, - payload ? payload["error"] : undefined, - payload ? payload["msg"] : undefined, - payload ? payload["message"] : undefined - ]; - - for (const candidate of candidates) { - if ((typeof candidate === "string" || typeof candidate === "number") && String(candidate).trim().length > 0) { - return String(candidate).trim(); - } - } - - return fallback; - } - - function normalizeStatus(status: unknown): string { - return typeof status === "string" ? status.trim().toLowerCase() : ""; - } - - function normalizeProgress(progress: unknown): number { - const normalized = typeof progress === "number" ? progress : Number(String(progress)); - return Number.isFinite(normalized) ? normalized : 0; - } - - function isSuccessStatus(status: string): boolean { - return status === "succeeded" || status === "success" || status === "completed" || status === "done"; - } - - function isFailureStatus(status: string): boolean { - return status === "failed" || status === "error" || status === "canceled" || status === "cancelled"; - } - - function extractImageUrlFromPayload(payload: Record): string { - const directCandidates: unknown[] = [ - payload["url"], - payload["image_url"], - payload["imageUrl"], - payload["file_url"], - payload["fileUrl"], - payload["result_url"] - ]; - - for (const candidate of directCandidates) { - if ((typeof candidate === "string" || typeof candidate === "number") && String(candidate).trim().length > 0) { - return String(candidate).trim(); - } - } - - const results = payload["results"]; - if (!Array.isArray(results) || results.length === 0) { - return ""; - } - - const first = results[0]; - if (isRecord(first)) { - const nestedCandidates: unknown[] = [ - first["url"], - first["image_url"], - first["imageUrl"], - first["file_url"], - first["fileUrl"], - first["result_url"] - ]; - for (const candidate of nestedCandidates) { - if ((typeof candidate === "string" || typeof candidate === "number") && String(candidate).trim().length > 0) { - return String(candidate).trim(); - } - } - } else if ((typeof first === "string" || typeof first === "number") && String(first).trim().length > 0) { - return String(first).trim(); - } - - return ""; - } - - async function uploadImageToBeeimg(filePath: string): Promise { - const exists = await Tools.Files.exists(filePath); - if (!exists.exists) { - throw new Error(`参考图文件不存在: ${filePath}`); - } - - const apiKey = getBeeimgApiKey(); - if (!apiKey) { - throw new Error("使用 image_paths 需要配置 BEEIMG_API_KEY(用于把本地图片上传到图床以获得公网URL)。"); - } - - const resp = await Tools.Net.uploadFile({ - url: BEEIMG_UPLOAD_ENDPOINT, - method: "POST", - form_data: { - apikey: apiKey - }, - files: [ - { - field_name: "file", - file_path: filePath, - content_type: guessMimeTypeFromPath(filePath) - } - ] - }); - - if (resp.statusCode < 200 || resp.statusCode >= 300) { - throw new Error(`BeeIMG 上传失败: HTTP ${resp.statusCode} - ${resp.content}`); - } - - let parsed: unknown; - try { - parsed = safeJsonParseLoose(resp.content); - } catch (e: unknown) { - throw new Error(`BeeIMG 上传响应解析失败: ${getErrorMessage(e)}`); - } - - const files = isRecord(parsed) && isRecord(parsed["files"]) ? (parsed["files"] as Record) : null; - const ok = !!files && (files["status"] === "Success" || files["code"] === "200" || files["code"] === 200); - const url = files ? files["url"] : undefined; - if (!ok || (typeof url !== "string" && typeof url !== "number") || String(url).trim().length === 0) { - throw new Error(`BeeIMG 上传失败: ${resp.content}`); - } - return String(url); - } - - function sanitizeFileName(name: string): string { - const safe = name.replace(/[\\/:*?"<>|]/g, "_").trim(); - if (!safe) { - return `nano_draw_${Date.now()}`; - } - return safe.substring(0, 80); - } - - function buildFileName(prompt: string, customName?: string | null): string { - if (customName && customName.trim().length > 0) { - return sanitizeFileName(customName); - } - const shortPrompt = prompt.length > 40 ? `${prompt.substring(0, 40)}...` : prompt; - const base = sanitizeFileName(shortPrompt || "image"); - const timestamp = Date.now(); - return `${base}_${timestamp}`; - } - - async function ensureDirectories(): Promise { - const dirs = [DRAW_ROOT_DIR, STORAGE_DIR, DRAWS_DIR]; - for (const dir of dirs) { - try { - const result = await Tools.Files.mkdir(dir); - if (!result.successful) { - console.warn(`创建目录失败(可能已存在): ${dir} -> ${result.details}`); - } - } catch (e: unknown) { - console.warn(`创建目录异常: ${dir} -> ${getErrorMessage(e)}`); - } - } - } - - async function callNanobananaApi(params: { - prompt: string; - model: string; - aspect_ratio?: string; - image_size?: string; - image_urls?: string[]; - poll_interval_ms?: number; - max_wait_time_ms?: number; - }): Promise { - const apiKey = getApiKey(); - const endpoint = getDrawEndpoint(getApiBaseUrl()); - - // 构建请求体 - 使用异步模式(webHook: "-1") - const body: Record = { - model: params.model, - prompt: params.prompt, - webHook: "-1", // 关键:立即返回任务ID - shutProgress: false - }; - - // 添加可选参数 - if (params.aspect_ratio && params.aspect_ratio.trim().length > 0) { - body.aspectRatio = params.aspect_ratio.trim(); - } - - if (params.image_size && params.image_size.trim().length > 0) { - body.imageSize = params.image_size.trim(); - } - - // 图生图:添加参考图URL数组 - // 支持格式:['https://example.com/1.jpg', 'https://example.com/2.jpg'] - // 或 JSON字符串:"[\"https://...\"]" - // 或 逗号分隔:"url1,url2" - if (params.image_urls && Array.isArray(params.image_urls) && params.image_urls.length > 0) { - body.urls = params.image_urls.filter(url => url && url.trim().length > 0); - } - - const headers = { - "accept": "application/json", - "content-type": "application/json", - "Authorization": `Bearer ${apiKey}` - }; - - const request = client - .newRequest() - .url(endpoint) - .method("POST") - .headers(headers) - .body(JSON.stringify(body), "json"); - - console.log("步骤1/2: 提交绘图任务..."); - const response = await request.build().execute(); - - if (!response.isSuccessful()) { - throw new Error(`Nano Banana API 调用失败: ${response.statusCode} - ${response.content}`); - } - - let parsed: unknown; - try { - parsed = JSON.parse(response.content) as unknown; - } catch (e: unknown) { - throw new Error(`解析 Nano Banana 响应失败: ${getErrorMessage(e)}`); - } - - if (!isRecord(parsed)) { - throw new Error("API响应不是合法对象,请检查参数是否正确。响应: " + response.content); - } - if (!isApiSuccessResponse(parsed)) { - throw new Error(`Nano Banana API 返回错误: ${extractMessage(parsed, response.content)}`); - } - - const payload = extractTaskPayload(parsed); - if (!payload || typeof payload["id"] !== "string") { - throw new Error("API响应中未找到任务ID,请检查参数是否正确。响应: " + JSON.stringify(parsed)); - } - - const taskId: string = payload["id"] as string; - console.log(`任务提交成功! ID: ${taskId}`); - const pollIntervalMs = params.poll_interval_ms ?? POLL_INTERVAL; - const maxWaitTimeMs = params.max_wait_time_ms ?? MAX_WAIT_TIME; - console.log(`步骤2/2: 等待任务完成(轮询中,每${pollIntervalMs / 1000}秒查询一次,最长等待${Math.ceil(maxWaitTimeMs / 60000)}分钟)...`); - - return taskId; - } - - async function pollForResult(taskId: string, options: { poll_interval_ms?: number; max_wait_time_ms?: number } = {}): Promise { - const apiKey = getApiKey(); - const endpoint = getResultEndpoint(getApiBaseUrl()); - const pollIntervalMs = normalizePositiveInt(options.poll_interval_ms, POLL_INTERVAL); - const maxWaitTimeMs = normalizePositiveInt(options.max_wait_time_ms, MAX_WAIT_TIME); - const startTime = Date.now(); - let attempt = 0; - - const doSleep = async (ms: number): Promise => { - await Tools.System.sleep(ms); - }; - - while (Date.now() - startTime < maxWaitTimeMs) { - attempt++; - console.log(`第${attempt}次查询任务状态...`); - - try { - const request = client - .newRequest() - .url(endpoint) - .method("POST") - .headers({ - "accept": "application/json", - "content-type": "application/json", - "Authorization": `Bearer ${apiKey}` - }) - .body(JSON.stringify({ id: taskId }), "json"); - - const response = await request.build().execute(); - - if (!response.isSuccessful()) { - console.warn(`⚠️ 查询请求未成功 (HTTP ${response.statusCode}): ${response.content},将重试...`); - await doSleep(pollIntervalMs); - continue; - } - - let parsed: unknown; - try { - parsed = JSON.parse(response.content) as unknown; - } catch (_error: unknown) { - console.warn("⚠️ 解析结果响应失败,将重试"); - await doSleep(pollIntervalMs); - continue; - } - - if (!isRecord(parsed)) { - console.warn(`查询响应异常: ${JSON.stringify(parsed)}`); - await doSleep(pollIntervalMs); - continue; - } - - if (parsed["code"] === -22 || parsed["code"] === "-22") { - console.log("任务排队/处理中... (等待服务器生成)"); - await doSleep(pollIntervalMs); - continue; - } - - if (!isApiSuccessResponse(parsed)) { - console.warn(`⚠️ API 返回异常状态,将重试: ${JSON.stringify(parsed)}`); - await doSleep(pollIntervalMs); - continue; - } - - const data = extractTaskPayload(parsed); - if (!data) { - console.warn(`查询响应异常 (无有效数据): ${JSON.stringify(parsed)}`); - await doSleep(pollIntervalMs); - continue; - } - - const progress = normalizeProgress(data["progress"]); - const status = normalizeStatus(data["status"]); - const imageUrl = extractImageUrlFromPayload(data); - - console.log(`当前进度: ${progress}% | 状态: ${status || "unknown"}`); - - if (isSuccessStatus(status) || (progress >= 100 && imageUrl.length > 0)) { - console.log("✅ 任务完成!"); - if (imageUrl.length === 0) { - throw new Error("任务完成但响应中未找到图片URL: " + JSON.stringify(data)); - } - return imageUrl; - } - if (isFailureStatus(status)) { - throw new Error(`任务执行失败: ${JSON.stringify(data)}`); - } - if ((status === "running" || status === "processing") && progress > 0) { - console.log(`生成中... 进度: ${progress}%`); - } - } catch (error: unknown) { - console.log(`⚠️ 第${attempt}次查询发生不可预知的异常: ${getErrorMessage(error)},程序将自动进行下一次尝试...`); - } - - await doSleep(pollIntervalMs); - } - - throw new Error(`任务超时: 等待超过${Math.ceil(maxWaitTimeMs / 60000)}分钟仍未完成`); - } - - function guessExtensionFromUrl(url: string): string { - const match = url.match(/\.(png|jpg|jpeg|webp|gif)(?:\?|#|$)/i); - if (match && match[1]) { - return match[1].toLowerCase(); - } - return "png"; - } - - interface DrawImageParams { - prompt: string; - model?: string; - model_variant?: string; - aspect_ratio?: string; - image_size?: string; - image_urls?: string[] | string; - image_paths?: string[] | string; - file_name?: string; - poll_interval_ms?: number | string; - max_wait_time_ms?: number | string; - } - - interface DrawImageResult { - file_path: string; - file_uri: string; - markdown: string; - prompt: string; - model: string; - aspect_ratio?: string; - image_size?: string; - image_urls?: string[] | string; - image_paths?: string[] | string; - hint: string; - } - - async function draw_image(params: DrawImageParams): Promise { - if (!params || !params.prompt || params.prompt.trim().length === 0) { - throw new Error("参数 prompt 不能为空。"); - } - - const prompt = params.prompt.trim(); - const resolvedModel = resolveModel(params.model, params.model_variant); - - if (params.image_size && params.image_size.trim().length > 0 && resolvedModel !== MODEL_PRO) { - throw new Error("参数 image_size 仅支持 pro 模型(model_variant='pro' 或 model='nano-banana-pro')。"); - } - - const pollIntervalMs = normalizePositiveInt(params.poll_interval_ms, POLL_INTERVAL); - const normalizedImageSize = params.image_size ? params.image_size.trim().toUpperCase() : ""; - const defaultMaxWaitTimeMs = normalizedImageSize === "4K" ? 600000 : MAX_WAIT_TIME; - const maxWaitTimeMs = normalizePositiveInt(params.max_wait_time_ms, defaultMaxWaitTimeMs); - - // 添加辅助函数来解析URL数组 - function parseImageUrls(image_urls: string[] | string): string[] { - // 如果已经是数组,直接过滤空值返回 - if (Array.isArray(image_urls)) { - return image_urls.filter(url => url && url.trim().length > 0); - } - - // 如果是字符串,尝试解析 - if (typeof image_urls === "string") { - // 方法一:尝试JSON解析 - try { - const parsed: unknown = JSON.parse(image_urls) as unknown; - if (Array.isArray(parsed)) { - return parsed.filter((url) => typeof url === "string" && url.trim().length > 0); - } - } catch (e) { - // 解析失败继续方法二 - } - - // 方法二:按逗号分割(支持 "url1,url2" 格式) - const splitUrls = image_urls.split(",") - .map(url => url.trim()) - .filter(url => url.length > 0); - if (splitUrls.length > 0) { - return splitUrls; - } - } - - return []; - } - - function parseImagePaths(image_paths: string[] | string): string[] { - if (Array.isArray(image_paths)) { - return image_paths.filter(p => p && String(p).trim().length > 0).map(p => String(p).trim()); - } - if (typeof image_paths === "string") { - try { - const parsed: unknown = JSON.parse(image_paths) as unknown; - if (Array.isArray(parsed)) { - return parsed.filter((p) => p && String(p).trim().length > 0).map((p) => String(p).trim()); - } - } catch (e) { - // ignore - } - - const splitPaths = image_paths.split(",") - .map(p => p.trim()) - .filter(p => p.length > 0); - if (splitPaths.length > 0) { - return splitPaths; - } - } - return []; - } - - // 替换原有的验证逻辑 - let imageUrlsArray: string[] = []; - if (params.image_urls) { - imageUrlsArray = parseImageUrls(params.image_urls); - if (imageUrlsArray.length === 0) { - throw new Error("参数 image_urls 必须是有效的URL数组。"); - } - } - - let imagePathsArray: string[] = []; - if (params.image_paths) { - imagePathsArray = parseImagePaths(params.image_paths); - if (imagePathsArray.length === 0) { - throw new Error("参数 image_paths 必须是有效的本地路径数组。"); - } - } - - if (imagePathsArray.length > 0) { - console.log(`检测到 ${imagePathsArray.length} 张本地参考图,开始上传以获得公网URL...`); - for (const p of imagePathsArray) { - const url = await uploadImageToBeeimg(p); - imageUrlsArray.push(url); - } - console.log("本地参考图上传完成。"); - } - - await ensureDirectories(); - - // 步骤1: 提交任务并获取任务ID - const taskId = await callNanobananaApi({ - prompt, - model: resolvedModel, - aspect_ratio: params.aspect_ratio, - image_size: params.image_size, - image_urls: imageUrlsArray, - poll_interval_ms: pollIntervalMs, - max_wait_time_ms: maxWaitTimeMs - }); - - // 步骤2: 轮询等待任务完成 - const imageUrl = await pollForResult(taskId, { poll_interval_ms: pollIntervalMs, max_wait_time_ms: maxWaitTimeMs }); - - const ext = guessExtensionFromUrl(imageUrl); - const baseName = buildFileName(prompt, params.file_name ?? null); - const filePath = `${DRAWS_DIR}/${baseName}.${ext}`; - - const downloadResult = await Tools.Files.download(imageUrl, filePath); - if (!downloadResult.successful) { - throw new Error(`下载图片失败: ${downloadResult.details}`); - } - - const fileUri = `file://${filePath}`; - const markdown = `![AI生成的图片](${fileUri})`; - - const hintLines: string[] = []; - hintLines.push(`图片已生成并保存在本地 ${DRAWS_DIR}。`); - hintLines.push(`本地路径: ${filePath}`); - hintLines.push(""); - hintLines.push("在后续回答中,请直接输出下面这一行 Markdown 来展示这张图片:"); - hintLines.push(""); - hintLines.push(markdown); - - return { - file_path: filePath, - file_uri: fileUri, - markdown, - prompt, - model: resolvedModel, - aspect_ratio: params.aspect_ratio, - image_size: params.image_size, - image_urls: params.image_urls, - image_paths: params.image_paths, - hint: hintLines.join("\n") - }; - } - - async function draw_image_wrapper(params: DrawImageParams) { - try { - const result = await draw_image(params); - complete({ - success: true, - message: `图片生成成功,已保存到 ${DRAWS_DIR},并返回 Markdown 图片提示。`, - data: result - }); - } catch (error: unknown) { - console.error("draw_image 执行失败:", error); - complete({ - success: false, - message: `图片生成失败: ${getErrorMessage(error)}`, - error_stack: getErrorStack(error) - }); - } - } - - return { - draw_image: draw_image_wrapper - }; -})(); - -exports.draw_image = nanobananaDraw.draw_image; diff --git a/scripts/network_test.ts b/scripts/network_test.ts deleted file mode 100644 index 5903c27..0000000 --- a/scripts/network_test.ts +++ /dev/null @@ -1,874 +0,0 @@ -/* -METADATA -{ - "name": "network_test", - - "display_name": { - "zh": "网络测试", - "en": "Network Test" - }, - "category": "Network", - "description": { "zh": "网络测试工具集合,提供基于OkHttp3的网络请求功能,包括GET、POST、PUT、DELETE请求方法,以及请求超时设置、重定向控制和拦截器管理。支持多种数据格式,便于测试API接口和网络连接性能。", "en": "Network testing tools based on OkHttp3. Provides GET/POST/PUT/DELETE requests, timeout settings, redirect control, and interceptor management. Supports multiple data formats for API testing and connectivity diagnostics." }, - "tools": [ - { - "name": "http_get", - "description": { "zh": "发送HTTP GET请求", "en": "Send an HTTP GET request." }, - "parameters": [ - { - "name": "url", - "description": { "zh": "请求URL", "en": "Request URL." }, - "type": "string", - "required": true - }, - { - "name": "headers", - "description": { "zh": "请求头", "en": "Request headers." }, - "type": "object", - "required": false - } - ] - }, - { - "name": "http_post", - "description": { "zh": "发送HTTP POST请求", "en": "Send an HTTP POST request." }, - "parameters": [ - { - "name": "url", - "description": { "zh": "请求URL", "en": "Request URL." }, - "type": "string", - "required": true - }, - { - "name": "body", - "description": { "zh": "请求体", "en": "Request body." }, - "type": "object", - "required": true - }, - { - "name": "headers", - "description": { "zh": "请求头", "en": "Request headers." }, - "type": "object", - "required": false - }, - { - "name": "body_type", - "description": { "zh": "请求体类型,支持'text'、'json'、'form'、'multipart'", "en": "Body type: 'text', 'json', 'form', or 'multipart'." }, - "type": "string", - "required": false - } - ] - }, - { - "name": "http_put", - "description": { "zh": "发送HTTP PUT请求", "en": "Send an HTTP PUT request." }, - "parameters": [ - { - "name": "url", - "description": { "zh": "请求URL", "en": "Request URL." }, - "type": "string", - "required": true - }, - { - "name": "body", - "description": { "zh": "请求体", "en": "Request body." }, - "type": "object", - "required": true - }, - { - "name": "headers", - "description": { "zh": "请求头", "en": "Request headers." }, - "type": "object", - "required": false - }, - { - "name": "body_type", - "description": { "zh": "请求体类型,支持'text'、'json'、'form'、'multipart'", "en": "Body type: 'text', 'json', 'form', or 'multipart'." }, - "type": "string", - "required": false - } - ] - }, - { - "name": "http_delete", - "description": { "zh": "发送HTTP DELETE请求", "en": "Send an HTTP DELETE request." }, - "parameters": [ - { - "name": "url", - "description": { "zh": "请求URL", "en": "Request URL." }, - "type": "string", - "required": true - }, - { - "name": "headers", - "description": { "zh": "请求头", "en": "Request headers." }, - "type": "object", - "required": false - } - ] - }, - { - "name": "config_client", - "description": { "zh": "配置HTTP客户端", "en": "Configure the HTTP client." }, - "parameters": [ - { - "name": "connect_timeout", - "description": { "zh": "连接超时时间(毫秒)", "en": "Connection timeout (milliseconds)." }, - "type": "number", - "required": false - }, - { - "name": "read_timeout", - "description": { "zh": "读取超时时间(毫秒)", "en": "Read timeout (milliseconds)." }, - "type": "number", - "required": false - }, - { - "name": "write_timeout", - "description": { "zh": "写入超时时间(毫秒)", "en": "Write timeout (milliseconds)." }, - "type": "number", - "required": false - }, - { - "name": "follow_redirects", - "description": { "zh": "是否跟随重定向", "en": "Whether to follow redirects." }, - "type": "boolean", - "required": false - }, - { - "name": "retry_on_failure", - "description": { "zh": "是否在连接失败时重试", "en": "Whether to retry on connection failure." }, - "type": "boolean", - "required": false - } - ] - }, - { - "name": "ping_test", - "description": { "zh": "测试与指定URL的网络连接", "en": "Test network connectivity to a specified URL." }, - "parameters": [ - { - "name": "url", - "description": { "zh": "要测试的URL", "en": "URL to test." }, - "type": "string", - "required": true - }, - { - "name": "count", - "description": { "zh": "测试次数", "en": "Number of test attempts." }, - "type": "number", - "required": false - } - ] - }, - { - "name": "test_all", - "description": { "zh": "运行所有网络测试", "en": "Run all network tests." }, - "parameters": [] - } - ] -}*/ - -const networkTest = (function () { - // 默认客户端 - let defaultClient = OkHttp.newClient(); - - // 客户端配置 - let clientConfig: OkHttpConfig = { - timeouts: { - connect: 10000, - read: 10000, - write: 10000 - }, - followRedirects: true, - retryOnConnectionFailure: true, - interceptors: [] - }; - - /** - * 配置HTTP客户端 - * @param params - 配置参数 - */ - async function config_client(params: { - connect_timeout?: number, - read_timeout?: number, - write_timeout?: number, - follow_redirects?: boolean, - retry_on_failure?: boolean - }): Promise { - try { - // 更新配置 - if (params.connect_timeout !== undefined) { - clientConfig.timeouts.connect = params.connect_timeout; - } - if (params.read_timeout !== undefined) { - clientConfig.timeouts.read = params.read_timeout; - } - if (params.write_timeout !== undefined) { - clientConfig.timeouts.write = params.write_timeout; - } - if (params.follow_redirects !== undefined) { - clientConfig.followRedirects = params.follow_redirects; - } - if (params.retry_on_failure !== undefined) { - clientConfig.retryOnConnectionFailure = params.retry_on_failure; - } - - // 创建新客户端 - const builder = OkHttp.newBuilder() - .connectTimeout(clientConfig.timeouts.connect) - .readTimeout(clientConfig.timeouts.read) - .writeTimeout(clientConfig.timeouts.write) - .followRedirects(clientConfig.followRedirects) - .retryOnConnectionFailure(clientConfig.retryOnConnectionFailure); - - // 添加拦截器 - if (clientConfig.interceptors.length > 0) { - for (const interceptor of clientConfig.interceptors) { - builder.addInterceptor(interceptor); - } - } - - // 构建客户端 - defaultClient = builder.build(); - - return { - success: true, - message: "HTTP客户端配置已更新", - config: { - ...clientConfig - } - }; - } catch (error) { - throw new Error(`配置HTTP客户端失败: ${error.message}`); - } - } - - /** - * 发送HTTP GET请求 - * @param params - 请求参数 - */ - async function http_get(params: { url: string, headers?: Record }): Promise { - try { - if (!params.url) { - throw new Error("URL不能为空"); - } - - console.log(`发送GET请求: ${params.url}`); - console.log(`请求头: ${JSON.stringify(params.headers || {})}`); - - // 准备请求 - const request = defaultClient.newRequest() - .url(params.url) - .method('GET'); - - // 添加请求头 - if (params.headers) { - request.headers(params.headers); - } - - // 执行请求 - const response = await request.build().execute(); - - // 返回结果 - const result = await formatResponse(response); - - // 额外输出重要的响应信息 - console.log(`\n🟢 GET请求完成: ${params.url}`); - console.log(`🟢 状态码: ${result.status_code} ${result.status_message}`); - if (result.json) { - console.log(`🟢 返回数据类型: ${Array.isArray(result.json) ? '数组' : (typeof result.json === 'object' ? '对象' : typeof result.json)}`); - } - console.log(`🟢 响应大小: ${result.content_length} 字节\n`); - - return result; - } catch (error) { - throw new Error(`GET请求失败: ${error.message}`); - } - } - - /** - * 发送HTTP POST请求 - * @param params - 请求参数 - */ - async function http_post(params: { - url: string, - body: any, - headers?: Record, - body_type?: 'text' | 'json' | 'form' | 'multipart' - }): Promise { - try { - if (!params.url) { - throw new Error("URL不能为空"); - } - - if (params.body === undefined) { - throw new Error("请求体不能为空"); - } - - console.log(`发送POST请求: ${params.url}`); - - // 准备请求 - const request = defaultClient.newRequest() - .url(params.url) - .method('POST'); - - // 添加请求头 - if (params.headers) { - request.headers(params.headers); - } - - // 添加请求体 - const bodyType = params.body_type || 'json'; - request.body(params.body, bodyType); - - // 执行请求 - const response = await request.build().execute(); - - // 返回结果 - return await formatResponse(response); - } catch (error) { - throw new Error(`POST请求失败: ${error.message}`); - } - } - - /** - * 发送HTTP PUT请求 - * @param params - 请求参数 - */ - async function http_put(params: { - url: string, - body: any, - headers?: Record, - body_type?: 'text' | 'json' | 'form' | 'multipart' - }): Promise { - try { - if (!params.url) { - throw new Error("URL不能为空"); - } - - if (params.body === undefined) { - throw new Error("请求体不能为空"); - } - - console.log(`发送PUT请求: ${params.url}`); - - // 准备请求 - const request = defaultClient.newRequest() - .url(params.url) - .method('PUT'); - - // 添加请求头 - if (params.headers) { - request.headers(params.headers); - } - - // 添加请求体 - const bodyType = params.body_type || 'json'; - request.body(params.body, bodyType); - - // 执行请求 - const response = await request.build().execute(); - - // 返回结果 - return await formatResponse(response); - } catch (error) { - throw new Error(`PUT请求失败: ${error.message}`); - } - } - - /** - * 发送HTTP DELETE请求 - * @param params - 请求参数 - */ - async function http_delete(params: { url: string, headers?: Record }): Promise { - try { - if (!params.url) { - throw new Error("URL不能为空"); - } - - console.log(`发送DELETE请求: ${params.url}`); - - // 准备请求 - const request = defaultClient.newRequest() - .url(params.url) - .method('DELETE'); - - // 添加请求头 - if (params.headers) { - request.headers(params.headers); - } - - // 执行请求 - const response = await request.build().execute(); - - // 返回结果 - return await formatResponse(response); - } catch (error) { - throw new Error(`DELETE请求失败: ${error.message}`); - } - } - - /** - * 格式化响应结果 - * @param response - OkHttp响应 - */ - async function formatResponse(response: OkHttpResponse): Promise { - try { - let responseBody = ''; - let jsonData: any = undefined; - - // 获取响应文本 - responseBody = response.content; - console.log(`\n===== 响应内容开始 =====`); - console.log(`状态码: ${response.statusCode} ${response.statusMessage}`); - console.log(`内容类型: ${response.contentType || '未知'}`); - - // 如果响应体不太长,完整显示 - if (responseBody && responseBody.length < 1000) { - console.log(`响应体:\n${responseBody}`); - } else if (responseBody) { - console.log(`响应体(截断):\n${responseBody.substring(0, 500)}...`); - console.log(`[完整长度: ${responseBody.length} 字符]`); - } else { - console.log(`响应体为空`); - } - - // 尝试解析JSON - if (response.contentType && response.contentType.includes('application/json')) { - try { - jsonData = response.json(); - if (jsonData) { - if (Array.isArray(jsonData)) { - console.log(`- 数组数据, 长度: ${(jsonData as any[]).length}`); - if ((jsonData as any[]).length > 0) { - console.log(`- 第一项样本: ${JSON.stringify(jsonData[0]).substring(0, 100)}${JSON.stringify(jsonData[0]).length > 100 ? '...' : ''}`); - } - } else if (typeof jsonData === 'object') { - const keys = Object.keys(jsonData); - console.log(`- 对象数据, 字段数: ${keys.length}`); - console.log(`- 字段列表: ${keys.slice(0, 5).join(', ')}${keys.length > 5 ? '...' : ''}`); - // 输出一个示例值 - if (keys.length > 0) { - const sampleKey = keys[0]; - const sampleValue = JSON.stringify(jsonData[sampleKey]); - console.log(`- 示例 "${sampleKey}": ${sampleValue.substring(0, 60)}${sampleValue.length > 60 ? '...' : ''}`); - } - - // 输出完整的JSON格式化数据 - console.log(`\nJSON完整数据:`); - console.log(JSON.stringify(jsonData, undefined, 2)); - } else { - console.log(`- 基本类型数据: ${JSON.stringify(jsonData).substring(0, 100)}`); - } - } - } catch (e) { - console.warn("响应内容不是有效的JSON格式"); - } - } - console.log(`===== 响应内容结束 =====\n`); - - return { - success: response.isSuccessful(), - status_code: response.statusCode, - status_message: response.statusMessage, - headers: response.headers, - content_type: response.contentType, - content_length: response.size, - body: responseBody, - json: jsonData, - time_info: { - timestamp: new Date().toISOString() - } - }; - } catch (error) { - console.error(`格式化响应出错: ${error.message}`); - return { - success: false, - error: `格式化响应出错: ${error.message}` - }; - } - } - - /** - * 测试与指定URL的网络连接 - * @param params - 测试参数 - */ - async function ping_test(params: { url: string, count?: number }): Promise { - try { - if (!params.url) { - throw new Error("URL不能为空"); - } - - const count = params.count || 3; - const results: Array<{ - attempt: number; - success: boolean; - time_ms: number; - status?: number; - error?: string; - }> = []; - let totalTime = 0; - let successCount = 0; - let failCount = 0; - - console.log(`开始Ping测试: ${params.url}, 次数: ${count}`); - - for (let i = 0; i < count; i++) { - const startTime = Date.now(); - try { - // 发送简单的HEAD请求来测试连接 - const request = defaultClient.newRequest() - .url(params.url) - .method('HEAD') - .build(); - - const response = await request.execute(); - const endTime = Date.now(); - const elapsed = endTime - startTime; - - totalTime += elapsed; - successCount++; - - results.push({ - attempt: i + 1, - success: true, - time_ms: elapsed, - status: response.statusCode - }); - - console.log(`Ping #${i + 1}: ${elapsed}ms, 状态: ${response.statusCode}`); - - // 等待一小段时间后再发送下一个请求 - if (i < count - 1) { - await sleep(500); - } - } catch (error) { - const endTime = Date.now(); - const elapsed = endTime - startTime; - failCount++; - - results.push({ - attempt: i + 1, - success: false, - time_ms: elapsed, - error: error.message - }); - - console.log(`Ping #${i + 1}: 失败, 错误: ${error.message}`); - - // 等待一小段时间后再发送下一个请求 - if (i < count - 1) { - await sleep(500); - } - } - } - - const avgTime = successCount > 0 ? totalTime / successCount : 0; - - return { - url: params.url, - success: successCount > 0, - summary: { - total_count: count, - success_count: successCount, - fail_count: failCount, - success_rate: `${(successCount / count * 100).toFixed(1)}%`, - average_time: `${avgTime.toFixed(2)}ms` - }, - detail_results: results - }; - } catch (error) { - throw new Error(`Ping测试失败: ${error.message}`); - } - } - - /** - * 等待指定的毫秒数 - * @param ms 等待的毫秒数 - */ - async function sleep(ms: number | string): Promise { - const sleepTime = Number(ms); - if (isNaN(sleepTime)) { - throw new Error("无效的等待时间"); - } - return new Promise(resolve => setTimeout(resolve, sleepTime)); - } - - /** - * 测试所有网络功能 - */ - async function test_all(): Promise { - try { - console.log("开始网络功能测试..."); - - const results: { - config?: any; - ping?: any; - get?: any; - post?: any; - put?: any; - delete?: any; - } = {}; - - // 1. 测试客户端配置 - console.log("测试客户端配置..."); - try { - const configResult = await config_client({ - connect_timeout: 8000, - read_timeout: 8000, - write_timeout: 8000, - follow_redirects: true, - retry_on_failure: true - }); - results.config = configResult; - console.log("✓ 客户端配置成功"); - } catch (error) { - results.config = { error: `客户端配置失败: ${error.message}` }; - console.log("✗ 客户端配置失败"); - } - - // 2. 测试连接 - console.log("测试网络连接..."); - try { - const pingResult = await ping_test({ - url: "https://httpbin.org", - count: 2 - }); - results.ping = pingResult; - console.log("✓ Ping测试成功"); - } catch (error) { - results.ping = { error: `Ping测试失败: ${error.message}` }; - console.log("✗ Ping测试失败"); - } - - // 3. 测试GET请求 - console.log("测试GET请求..."); - try { - const getResult = await http_get({ - url: "https://httpbin.org/get", - headers: { - "User-Agent": "OkHttp-Network-Tester/1.0" - } - }); - results.get = getResult; - console.log("✓ GET请求成功"); - - // 显示更多关于GET响应的信息 - console.log("\n====== GET请求结果摘要 ======"); - console.log(`状态: ${getResult.status_code} ${getResult.status_message}`); - - // 如果有JSON数据,显示一些关键信息 - if (getResult.json) { - console.log("GET响应数据预览:"); - - // httpbin.org/get 通常会返回请求信息的镜像 - if (getResult.json.headers) { - console.log("- 发送的请求头:"); - Object.keys(getResult.json.headers).forEach(key => { - console.log(` ${key}: ${getResult.json.headers[key]}`); - }); - } - - if (getResult.json.url) { - console.log(`- 请求URL: ${getResult.json.url}`); - } - - if (getResult.json.origin) { - console.log(`- 来源IP: ${getResult.json.origin}`); - } - } - console.log("===============================\n"); - } catch (error) { - results.get = { error: `GET请求失败: ${error.message}` }; - console.log("✗ GET请求失败"); - } - - // 4. 测试POST请求 - console.log("测试POST请求..."); - try { - const postResult = await http_post({ - url: "https://httpbin.org/post", - body: { - name: "OkHttp网络测试", - timestamp: new Date().toISOString() - }, - headers: { - "Content-Type": "application/json", - "User-Agent": "OkHttp-Network-Tester/1.0" - } - }); - results.post = postResult; - console.log("✓ POST请求成功"); - } catch (error) { - results.post = { error: `POST请求失败: ${error.message}` }; - console.log("✗ POST请求失败"); - } - - // 5. 测试PUT请求 - console.log("测试PUT请求..."); - try { - const putResult = await http_put({ - url: "https://httpbin.org/put", - body: { - name: "OkHttp网络测试", - timestamp: new Date().toISOString(), - action: "update" - }, - headers: { - "Content-Type": "application/json", - "User-Agent": "OkHttp-Network-Tester/1.0" - } - }); - results.put = putResult; - console.log("✓ PUT请求成功"); - } catch (error) { - results.put = { error: `PUT请求失败: ${error.message}` }; - console.log("✗ PUT请求失败"); - } - - // 6. 测试DELETE请求 - console.log("测试DELETE请求..."); - try { - const deleteResult = await http_delete({ - url: "https://httpbin.org/delete", - headers: { - "User-Agent": "OkHttp-Network-Tester/1.0" - } - }); - results.delete = deleteResult; - console.log("✓ DELETE请求成功"); - } catch (error) { - results.delete = { error: `DELETE请求失败: ${error.message}` }; - console.log("✗ DELETE请求失败"); - } - - // 返回所有测试结果 - return { - message: "网络功能测试完成", - test_results: results, - timestamp: new Date().toISOString(), - summary: "测试了各种网络功能,包括配置、Ping测试和HTTP请求。请查看各功能的测试结果。" - }; - } catch (error) { - return { - success: false, - message: `测试过程中发生错误: ${error.message}` - }; - } - } - - /** - * 包装函数 - 统一处理所有network_test函数的返回结果 - * @param func 原始函数 - * @param params 函数参数 - * @param successMessage 成功消息 - * @param failMessage 失败消息 - * @param additionalInfo 附加信息(可选) - */ - async function network_wrap( - func: (params: any) => Promise, - params: any, - successMessage: string, - failMessage: string, - additionalInfo: string = "" - ): Promise { - try { - console.log(`开始执行函数: ${func.name || '匿名函数'}`); - console.log(`参数:`, JSON.stringify(params, undefined, 2)); - - // 执行原始函数 - const result = await func(params); - - console.log(`函数 ${func.name || '匿名函数'} 执行结果:`, JSON.stringify(result, undefined, 2)); - - // 如果原始函数已经调用了complete,就不需要再次调用 - if (result === undefined) return; - - // 根据结果类型处理 - if (typeof result === "boolean") { - // 布尔类型结果 - complete({ - success: result, - message: result ? successMessage : failMessage, - additionalInfo: additionalInfo - }); - } else { - // 数据类型结果 - complete({ - success: true, - message: successMessage, - additionalInfo: additionalInfo, - data: result - }); - } - } catch (error) { - // 详细记录错误信息 - console.error(`函数 ${func.name || '匿名函数'} 执行失败!`); - console.error(`错误信息: ${error.message}`); - console.error(`错误堆栈: ${error.stack}`); - - // 处理错误 - complete({ - success: false, - message: `${failMessage}: ${error.message}`, - additionalInfo: additionalInfo, - error_stack: error.stack - }); - } - } - - return { - config_client: async (params) => await network_wrap( - config_client, - params, - "配置HTTP客户端成功", - "配置HTTP客户端失败" - ), - http_get: async (params) => await network_wrap( - http_get, - params, - "GET请求成功", - "GET请求失败" - ), - http_post: async (params) => await network_wrap( - http_post, - params, - "POST请求成功", - "POST请求失败" - ), - http_put: async (params) => await network_wrap( - http_put, - params, - "PUT请求成功", - "PUT请求失败" - ), - http_delete: async (params) => await network_wrap( - http_delete, - params, - "DELETE请求成功", - "DELETE请求失败" - ), - ping_test: async (params) => await network_wrap( - ping_test, - params, - "网络连接测试成功", - "网络连接测试失败" - ), - test_all: async () => await network_wrap( - test_all, - {}, - "网络功能测试完成", - "网络功能测试失败" - ) - }; -})(); - -// 导出所有函数 -exports.config_client = networkTest.config_client; -exports.http_get = networkTest.http_get; -exports.http_post = networkTest.http_post; -exports.http_put = networkTest.http_put; -exports.http_delete = networkTest.http_delete; -exports.ping_test = networkTest.ping_test; -exports.test_all = networkTest.test_all; -exports.main = networkTest.test_all; \ No newline at end of file diff --git a/scripts/openai_draw.ts b/scripts/openai_draw.ts deleted file mode 100644 index 017cae5..0000000 --- a/scripts/openai_draw.ts +++ /dev/null @@ -1,338 +0,0 @@ -/* METADATA -{ - "name": "openai_draw", - - "display_name": { - "zh": "OpenAI 绘图", - "en": "OpenAI Draw" - }, - "description": { - "zh": "使用 OpenAI 格式的图像生成 API (/v1/images/generations) 根据提示词画图,将图片保存到本地 /sdcard/Download/Operit/plugins/draw/openai_draw/draws/ 目录,并返回 Markdown 图片提示。", - "en": "Generate images via an OpenAI-compatible image generation API (/v1/images/generations) from a prompt, save to /sdcard/Download/Operit/plugins/draw/openai_draw/draws/, and return a Markdown image reference." - }, - "category": "Draw", - "env": [ - { - "name": "OPENAI_API_KEY", - "description": { - "zh": "OpenAI API Key(必填)", - "en": "OpenAI API key (required)" - }, - "required": true - }, - { - "name": "OPENAI_API_BASE_URL", - "description": { - "zh": "OpenAI API Base URL(可选,不填则默认 https://api.openai.com )", - "en": "OpenAI API base URL (optional; defaults to https://api.openai.com)" - }, - "required": false - }, - { - "name": "OPENAI_IMAGE_MODEL", - "description": { - "zh": "默认绘图模型(可选;当 draw_image 未传 model 时使用)", - "en": "Default image model (optional; used when draw_image doesn't pass model)" - }, - "required": false - } - ], - "tools": [ - { - "name": "draw_image", - "description": { - "zh": "根据提示词调用 OpenAI 格式图像生成接口生成图片,保存到本地并返回 Markdown 图片提示。", - "en": "Generate an image via an OpenAI-compatible image generation endpoint using a prompt, save it locally, and return a Markdown image reference." - }, - "parameters": [ - { "name": "prompt", "description": { "zh": "绘图提示词(英文或中文皆可)", "en": "Prompt for image generation (Chinese or English)" }, "type": "string", "required": true }, - { "name": "model", "description": { "zh": "模型名称(可选;不传则使用环境变量 OPENAI_IMAGE_MODEL,再不行使用默认值)", "en": "Model name (optional; falls back to env OPENAI_IMAGE_MODEL, then default)" }, "type": "string", "required": false }, - { "name": "size", "description": { "zh": "图片尺寸,例如 '1024x1024',可选", "en": "Image size, e.g. '1024x1024' (optional)" }, "type": "string", "required": false }, - { "name": "file_name", "description": { "zh": "自定义保存到本地的文件名(不含路径和扩展名)", "en": "Custom output file name (without path or extension)" }, "type": "string", "required": false }, - { "name": "api_base_url", "description": { "zh": "OpenAI API Base URL(不传则取环境变量 OPENAI_API_BASE_URL 或默认 https://api.openai.com )", "en": "OpenAI API base URL (optional; falls back to env OPENAI_API_BASE_URL or https://api.openai.com)" }, "type": "string", "required": false } - ] - } - ] -}*/ - -/// - -const openaiDraw = (function () { - const HTTP_TIMEOUT_MS = 600000; - const client = OkHttp.newBuilder() - .connectTimeout(HTTP_TIMEOUT_MS) - .readTimeout(HTTP_TIMEOUT_MS) - .writeTimeout(HTTP_TIMEOUT_MS) - .build(); - const DEFAULT_API_BASE_URL = "https://api.openai.com"; - const DEFAULT_MODEL = "gpt-image-1"; - - const DRAW_ROOT_DIR = getPluginConfigDir("draw"); - const STORAGE_DIR = `${DRAW_ROOT_DIR}/openai_draw`; - const DRAWS_DIR = `${STORAGE_DIR}/draws`; - - function getApiKey(): string { - const apiKey = getEnv("OPENAI_API_KEY"); - if (!apiKey) { - throw new Error("OPENAI_API_KEY 未配置,请在环境变量中设置 OpenAI 的 API Key。"); - } - return apiKey; - } - - function joinUrl(baseUrl: string, path: string): string { - const normalizedBase = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`; - const normalizedPath = path.startsWith('/') ? path.slice(1) : path; - return `${normalizedBase}${normalizedPath}`; - } - - function getApiBaseUrl(customBaseUrl?: string | null): string { - const fromParam = (customBaseUrl || "").trim(); - if (fromParam) return fromParam; - - const fromEnv = (getEnv("OPENAI_API_BASE_URL") || "").trim(); - if (fromEnv) return fromEnv; - - return DEFAULT_API_BASE_URL; - } - - function getImageEndpoint(baseUrl: string): string { - const trimmed = baseUrl.trim(); - if (!trimmed) return joinUrl(DEFAULT_API_BASE_URL, "v1/images/generations"); - - if (trimmed.includes("/v1/images/generations")) return trimmed; - - // If user passes .../v1, we should append images/generations - if (trimmed.endsWith("/v1")) return joinUrl(trimmed, "images/generations"); - if (trimmed.endsWith("/v1/")) return joinUrl(trimmed, "images/generations"); - - // Otherwise append v1/images/generations - return joinUrl(trimmed, "v1/images/generations"); - } - - function sanitizeFileName(name: string): string { - const safe = name.replace(/[\\/:*?"<>|]/g, "_").trim(); - if (!safe) { - return `openai_draw_${Date.now()}`; - } - return safe.substring(0, 80); - } - - function buildFileName(prompt: string, customName?: string | null): string { - if (customName && customName.trim().length > 0) { - return sanitizeFileName(customName); - } - const shortPrompt = prompt.length > 40 ? `${prompt.substring(0, 40)}...` : prompt; - const base = sanitizeFileName(shortPrompt || "image"); - const timestamp = Date.now(); - return `${base}_${timestamp}`; - } - - async function ensureDirectories() { - const dirs = [DRAW_ROOT_DIR, STORAGE_DIR, DRAWS_DIR]; - for (const dir of dirs) { - try { - const result = await Tools.Files.mkdir(dir); - if (!result.successful) { - console.warn(`创建目录失败(可能已存在): ${dir} -> ${result.details}`); - } - } catch (e: any) { - console.warn(`创建目录异常: ${dir} -> ${e?.message || e}`); - } - } - } - - function normalizeBase64(base64: string): string { - const raw = String(base64 || "").trim(); - if (!raw) return raw; - - // Some providers may return: data:image/png;base64,xxxx - const prefixIndex = raw.indexOf("base64,"); - if (raw.startsWith("data:") && prefixIndex >= 0) { - return raw.substring(prefixIndex + "base64,".length).trim(); - } - - return raw; - } - - type OpenAIImageApiResult = { - b64_json: string; - revised_prompt?: string; - effective_model: string; - }; - - async function callOpenAIImageApi(params: { - prompt: string; - model?: string; - size?: string; - api_base_url?: string; - }): Promise { - const apiKey = getApiKey(); - const apiBaseUrl = getApiBaseUrl(params.api_base_url); - const endpoint = getImageEndpoint(apiBaseUrl); - - const modelFromParam = (params.model || "").trim(); - const modelFromEnv = (getEnv("OPENAI_IMAGE_MODEL") || "").trim(); - const effectiveModel = modelFromParam || modelFromEnv || DEFAULT_MODEL; - - const body: any = { - model: effectiveModel, - prompt: params.prompt, - response_format: "b64_json" - }; - - if (params.size && params.size.trim().length > 0) { - body.size = params.size.trim(); - } - - const headers = { - "accept": "application/json", - "content-type": "application/json", - "Authorization": `Bearer ${apiKey}` - }; - - const request = client - .newRequest() - .url(endpoint) - .method("POST") - .headers(headers) - .body(JSON.stringify(body), "json"); - - const response = await request.build().execute(); - - if (!response.isSuccessful()) { - throw new Error(`OpenAI 图片 API 调用失败: ${response.statusCode} - ${response.content}`); - } - - let parsed: any; - try { - parsed = JSON.parse(response.content); - } catch (e: any) { - throw new Error(`解析 OpenAI 响应失败: ${e?.message || e}`); - } - - const item = parsed?.data?.[0]; - if (!item) { - throw new Error("OpenAI 响应中未找到 data[0],请检查接口返回格式。"); - } - - if (item.b64_json && String(item.b64_json).trim().length > 0) { - return { - b64_json: String(item.b64_json), - revised_prompt: item.revised_prompt ? String(item.revised_prompt) : undefined, - effective_model: effectiveModel - }; - } - - if (item.url && String(item.url).trim().length > 0) { - // Some OpenAI-compatible APIs may return url only. We download it, then read as base64. - const tmpName = `openai_tmp_${Date.now()}`; - const tmpPath = `${DRAWS_DIR}/${tmpName}.png`; - const downloadResult = await Tools.Files.download(String(item.url), tmpPath); - if (!downloadResult.successful) { - throw new Error(`下载图片失败: ${downloadResult.details}`); - } - - const readBinary = await Tools.Files.readBinary(tmpPath); - const contentBase64 = (readBinary as any)?.contentBase64; - if (!contentBase64 || String(contentBase64).trim().length === 0) { - throw new Error("读取下载图片失败: contentBase64 为空"); - } - - try { - await Tools.Files.deleteFile(tmpPath); - } catch { - // ignore - } - - return { - b64_json: String(contentBase64), - revised_prompt: item.revised_prompt ? String(item.revised_prompt) : undefined, - effective_model: effectiveModel - }; - } - - throw new Error("OpenAI 响应中未找到 b64_json 或 url,请检查模型/参数以及接口兼容性。"); - } - - async function draw_image(params: { - prompt: string; - model?: string; - size?: string; - file_name?: string; - api_base_url?: string; - }) { - if (!params || !params.prompt || params.prompt.trim().length === 0) { - throw new Error("参数 prompt 不能为空。"); - } - - const prompt = params.prompt.trim(); - - await ensureDirectories(); - - const apiResult = await callOpenAIImageApi({ - prompt, - model: params.model, - size: params.size, - api_base_url: params.api_base_url - }); - - const baseName = buildFileName(prompt, params.file_name); - const filePath = `${DRAWS_DIR}/${baseName}.png`; - - const writeResult = await Tools.Files.writeBinary(filePath, normalizeBase64(apiResult.b64_json)); - if (!writeResult.successful) { - throw new Error(`保存图片失败: ${writeResult.details}`); - } - - const fileUri = `file://${filePath}`; - const markdown = `![AI生成的图片](${fileUri})`; - - const hintLines: string[] = []; - hintLines.push(`图片已生成并保存在本地 ${DRAWS_DIR}。`); - hintLines.push(`本地路径: ${filePath}`); - hintLines.push(""); - hintLines.push("在后续回答中,请直接输出下面这一行 Markdown 来展示这张图片:"); - hintLines.push(""); - hintLines.push(markdown); - - return { - file_path: filePath, - file_uri: fileUri, - markdown, - prompt, - revised_prompt: apiResult.revised_prompt || null, - model: apiResult.effective_model, - hint: hintLines.join("\n") - }; - } - - async function draw_image_wrapper(params: { - prompt: string; - model?: string; - size?: string; - file_name?: string; - api_base_url?: string; - }) { - try { - const result = await draw_image(params); - complete({ - success: true, - message: `图片生成成功,已保存到 ${DRAWS_DIR},并返回 Markdown 图片提示。`, - data: result - }); - } catch (error: any) { - console.error("draw_image 执行失败:", error); - complete({ - success: false, - message: `图片生成失败: ${error?.message || error}`, - error_stack: error?.stack - }); - } - } - - return { - draw_image: draw_image_wrapper - }; -})(); - -exports.draw_image = openaiDraw.draw_image; diff --git a/scripts/operit_editor.ts b/scripts/operit_editor.ts deleted file mode 100644 index 7dc933a..0000000 --- a/scripts/operit_editor.ts +++ /dev/null @@ -1,3856 +0,0 @@ -/* METADATA -{ - name: "operit_editor" - display_name: { - zh: "Operit平台编辑器" - en: "Operit Platform Editor" - } - description: { - zh: '''Operit 平台配置直改工具包:提供一组可直接读取与修改 Operit 平台设置的工具,覆盖 MCP、Skill、Sandbox Package、功能模型绑定、模型参数、上下文总结与 TTS/STT 语音服务配置。''' - en: '''Direct Operit platform configuration toolkit: a collection of tools for reading and directly modifying Operit platform settings, covering MCP, Skill, Sandbox Package, function-model bindings, model parameters, context-summary settings, and TTS/STT speech-service configuration.''' - } - - enabledByDefault: true - - "category": "Chat", - tools: [ - { - name: "operit_editor" - description: { - zh: '''配置排查手册。 - -【触发条件】 -- 用户提到 MCP/Skill 安装失败、无法启动、工具不出现、导入失败、重名冲突、配置文件怎么改 -- 用户提到沙盒包(Package)开关、内置包列表、导入删除路径、包启用状态异常 -- 用户让你排查 Operit 的插件配置路径、部署目录、开关状态、环境变量 -- 用户提到功能模型绑定、模型配置新增/删除/修改、模型连接测试 -- 用户提到 TTS/STT 语音服务不会配置、参数太多不会填、语音播报/语音识别不可用 -- 问题核心是“配置和部署链路”,而不是普通问答 - -【MCP:安装与排查】 -1) 配置目录:/sdcard/Download/Operit/mcp_plugins/ -- 主配置:mcp_config.json -- 运行状态缓存:server_status.json(非实时快照,仅用于状态记录与工具缓存,不作为排查判定依据) -2) 两侧路径要严格区分: -- Android 侧是源目录(用户导入/存放目录),不是最终运行目录 -- Linux 侧是最终运行目录:~/mcp_plugins/ -- MCP 真正启动时,执行目录与命令都以 Linux 侧为准 -3) 本地部署实际行为(代码逻辑): -- 创建目标目录 -- 将 Android 侧插件目录复制到 Linux 侧目录 -- 执行自动分析出的安装/构建命令(会跳过启动命令) -3.1) 本地插件识别规则: -- 这样识别是为兼容存储位置外层套目录的情况(如 `/-main/...`);Android 侧目录内至少要命中一个标志文件:`README.md`、`package.json`、`mcp.config.json`、`main.js`、`main.py`、`index.js`、`index.py`,否则可能被判定为未安装,进不了启动列表。 -4) 命令型插件判定: -- 对于 command 为 npx/uvx/uv 的命令型插件,系统按“已部署”处理,仅做最小目录准备。 -- 配置 Node 类命令型 MCP 时,mcp_config.json 里仍应按上游常见写法填写 `command: "npx"`;不要自行改写成 `pnpm` 或 `npm`。 -- 软件内部启动这类 `npx` MCP 时,会自动把 `npx` 改写为 `pnpm dlx` 执行,并去掉 `-y`/`--yes` 一类确认参数。 -- 因此这类 MCP 的实际运行依赖是 `pnpm`;Linux 终端里必须装有 `pnpm`,不能只装 `npm`。 -- 不要把 command 改成 `npm`;当前已知这样可能触发 `double free` 报错。现阶段约定是:配置时按 `npx` 填,运行时由软件内部转成 `pnpm dlx`。 -5) 系统启动行为(必须理解): -- 本地插件:系统会读取 mcp_config.json 中该插件的 command/args/env,使用 cwd=~/mcp_plugins/ 启动进程;可用性以“工具可调用/服务可响应”为准 -- 远程插件:系统按 endpoint + connectionType(可选 bearerToken/headers)发起连接并校验连通 -6) command 的实际执行位置(必须按此理解): -- 本地插件启动时,系统在 Linux 终端环境中启动进程;执行工作目录固定是 ~/mcp_plugins/ -- mcp_config.json 里填写的 command 就是在这个 Linux 工作目录上下文中被执行,不会在 Android 源目录执行 -- args 里的相对路径,统一按该 cwd 解析 -6.1) node 命令示例(按插件ID): -- 例:pluginId=owner/my-plugin,则 shortName=my-plugin,cwd=~/mcp_plugins/my-plugin -- 如果入口文件在 ~/mcp_plugins/my-plugin/dist/index.js,则写: - "command": "node" - "args": ["dist/index.js"] -- 这里 args 必须按 cwd 写相对路径,不要写 /sdcard/... 的 Android 路径 -7) mcp_config.json 字段规范: -- 顶层必须有 mcpServers(对象) -- mcpServers 的 key 是 serverId(通常与插件ID一致,避免随意改名) -- pluginId(即 mcpServers 的 key)只允许 a-zA-Z_ 和空格 -- 每个 server 常用字段: - - command(必填,启动命令) - - args(可选,字符串数组) - - env(可选,键值对) - - autoApprove(可选,数组) - - disabled(可选,true=禁用) -- MCP 的环境变量必须写在 mcpServers..env;`read_environment_variable` / `write_environment_variable` 不会读写这里。 -8) mcp_config.json 路径写法规则: -- 不要把 Android 绝对路径写进本地插件启动参数(例如 /sdcard/...) -- 本地插件命令应按 Linux 运行目录编写,优先相对路径(因为 cwd 已固定到 ~/mcp_plugins/) -- 如果必须写绝对路径,也应是 Linux 侧路径,不应写 Android 侧路径 -9) 启用开关: -- 本地插件使用 mcpServers..disabled -- 远程插件使用 pluginMetadata..disabled -10) MCP 排查顺序(按顺序执行): -- 检查开关是否启用 -- 检查本地部署目录是否存在且非空 -- 检查 mcp_config.json 的 command/args/env 字段是否完整 -- 检查 args 是否误写 Android 路径 -- 检查 env 中所需 key/token -- 不要把 server_status.json 的 active 当作唯一依据;优先看工具是否可拉取、可调用 -- 检查终端依赖(node/pnpm/python/uv)与 MCP 服务状态;其中 Node 类 `npx` MCP 实际依赖 `pnpm`,若缺少 `pnpm` 将无法启动 - -【Skill:安装与排查】 -1) 目录:/sdcard/Download/Operit/skills/ -2) 识别规则:每个 Skill 必须是一个文件夹,且包含 SKILL.md(skill.md 也可)。 -3) 添加方式(按这个做): -- 先从可信来源下载 Skill(zip 或仓库源码均可) -- 把下载内容解压后,直接放到 /sdcard/Download/Operit/skills/ -- 最终目录结构必须是 /sdcard/Download/Operit/skills//,且该目录内有 SKILL.md -4) 元数据解析: -- 优先读取 frontmatter 中的 name/description -- 若缺失,回退读取文件前40行里的 name:/description: -5) AI 可见性: -- 列表开关关闭后,Skill 仍在本地,但 AI 不会使用 -6) Skill 排查顺序: -- 先确认路径和 SKILL.md -- 再确认是否重名冲突 -- 再确认开关是否开启 -- 最后检查 SKILL.md 内容是否完整(步骤/约束/输出) - -【Sandbox Package:安装与排查】 -1) 沙盒包目录(外部): -- Android/data/com.ai.assistance.operit/files/packages -2) 内置包: -- 内置包来自应用内置资源,不在上述外部目录;删除内置包文件不是常规操作,通常只做开关管理。 -3) 导入与删除: -- 导入:把 `.js` 或 `.toolpkg` 放入/导入到外部 packages 目录。 -- 删除:外部包可直接按文件路径删除;删除前先确认是否仍被依赖。 -4) 开关管理(优先使用工具): -- 先调用 list_sandbox_packages 获取“内置+外部”包列表与当前 enabled 状态。 -- 再调用 set_sandbox_package_enabled(package_name, enabled) 执行启停。 -5) 制作包文档: -- https://cdn.jsdelivr.net/gh/AAswordman/Operit@main/docs/SCRIPT_DEV_SKILL.md -- 该地址可直接通过 HTTP GET 请求访问,用于拉取原始 Markdown 文档内容。 -6) 软件内调试烧录: -- 普通 `.js` 沙盒包优先用 `debug_install_js_package`。 -- `ToolPkg` 优先用 `debug_install_toolpkg`;它会处理目录/manifest/.toolpkg 的打包或安装,并触发 ToolPkg 的刷新链路。 - -【Package 兼容模型(重要)】 -- AI 看到的 package 是统一抽象,底层可能来自 MCP、Skill、Sandbox Package 任意一种。 -- 可用包列表中的条目,不保证同类型;可能是三种类型混合出现。 -- `use_package` 是三兼容入口:对 MCP/Skill/Sandbox Package 都可统一调用。 -- `ping_mcp` 工具是 `use_package` 的直通封装,用于快速探测指定包是否可被加载。 - -【市场 Agent API(HTTP,只读)】 -- 基础前缀:`https://api.operit.app/market-stats` -- 搜索:`/agent/search?q=<关键词>&type=mcp|skill|package|script&limit=10` -- 详情:`/agent/items//` -- 安装计划:`/agent/items///install-plan` -- `package` / `script` 的 install_plan 通常会返回 `download_url`、`tracked_download_url`、`sha256`、`runtime_package_id`。 -- `skill` 的 install_plan 通常会返回 `repository_url`。 -- `mcp` 的 install_plan 可能返回 `config`(可直接作为 installConfig 参考)或 `repository_url`。 -- 当前软件未对 JS 暴露统一的一键安装市场接口;需要安装时,按条目类型自行下载、解压、导入,必要时配合 `debug_install_js_package` / `debug_install_toolpkg` / `use_package`。 - -【功能模型与模型配置】 -1) 模型配置(Model Config): -- 每个模型配置是一套完整连接参数(provider / endpoint / api key / model_name / custom_headers / 各能力开关)。 -- 可以新增、删除、修改;删除默认配置 `default` 是禁止的。 -2) 功能模型(Function Model): -- 每个功能类型(如 CHAT/SUMMARY/TRANSLATION 等)会绑定到一个模型配置。 -- 当一个配置里 `model_name` 写了多个模型(逗号分隔),还要指定 `model_index` 选择第几个。 -3) 关键工具(优先使用): -- `list_model_configs`:查看全部模型配置 + 当前功能绑定。 -- `create_model_config`:新增模型配置(可带初始 provider/endpoint/key/model_name/custom_headers)。 -- `update_model_config`:修改已有配置(按 config_id)。 -- `delete_model_config`:删除配置(默认配置不可删)。 -- `list_function_model_configs`:仅列出功能 -> 配置绑定关系(轻量)。 -- `get_function_model_config`:查看某个功能当前绑定的单个配置详情。 -- `set_function_model_config`:为功能指定配置与模型索引。 -- `test_model_config_connection`:按设置页同等逻辑测试配置连通与多模态能力。 -4) 配置修改后: -- 若该配置被某些功能使用,系统会刷新对应功能服务;绑定变更也会刷新目标功能服务。 -5) 用户抱怨“输出总被截断”时: -- 可先用 `get_function_model_config` 查看对应功能绑定配置里的 `max_tokens` 参数。 -- 对 DeepSeek 来说,默认常见是 4096;可将 `max_tokens_enabled` 打开并把 `max_tokens` 设到 8192 再测试。 -- 对其他模型,先联网确认该模型可支持的输出上限后再设置。 - -【上下文总结模块(Context Summary)】 -1) 用途: -- 控制会话上下文窗口与“何时触发总结”,用于降低上下文膨胀导致的丢信息、跑偏或响应不稳定。 -- 只有当前对话功能模型(CHAT)所绑定配置里的上下文总结参数,会在实际对话中生效。 -2) 核心机制说明: -- 软件内有一个 max 开关,即 `enable_max_context_mode`。 -- 开启时使用 `max_context_length`,关闭时使用 `context_length`,目标是为用户节约 token,并按场景动态决定使用长度。 -- `enable_summary` 控制是否自动总结;当上下文超过 `summary_token_threshold * 当前可用上下文长度` 时,会触发总结。 -3) 建议排查顺序: -- 先用 `get_context_summary_config` 看当前功能绑定配置的上下文总结参数。 -- 再用 `set_context_summary_config` 设置上下文总结参数(可传入参数覆盖默认值)。 -- 若仍异常,再结合具体模型能力与业务负载做细调。 - -【TTS/STT 语音服务配置】 -1) 配置入口: -- 在软件设置里的 Speech Services(语音服务)页面配置。 -- 该页面会自动保存;改完后语音服务实例会自动重建。 -2) TTS(文本转语音)可选引擎: -- `SIMPLE_TTS`:系统 TTS,基本无需填网络参数。 -- `HTTP_TTS`:需重点填写 `url_template`、`headers`、`http_method`、`content_type`、`request_body`;若服务先返回 JSON / 字段 / 下载链接,再额外填写 `response_pipeline`。 -- `OPENAI_WS_TTS`:需填写 `url_template`、`api_key`、`model_name`、`voice_id`。其中 `url_template` 应为 Realtime WebSocket 地址(例如 `wss://api.openai.com/v1/realtime`)。 -- `SILICONFLOW_TTS`:需填写 `api_key`、`model_name`、`voice_id`。 -- `MINIMAX_TTS`:需填写 `api_key`,可选填写 `url_template`、`model_name`、`voice_id`。默认接口为 `https://api.minimaxi.com/v1/t2a_v2`,内部固定按 `data.audio -> http_get` 解析音频。 -- `MIMO_TTS`:需填写 `api_key`,可选填写 `url_template`、`model_name`、`voice_id`。预置音色使用 `mimo-v2.5-tts` 和短 `voice_id`;声音克隆使用 `mimo-v2.5-tts-voiceclone`,`voice_id` 填完整 `data:audio/...;base64,...` 音频样本。默认接口为 `https://api.xiaomimimo.com/v1/chat/completions`,内部固定按 `choices[0].message.audio.data -> base64_decode` 解析音频。 -- `DOUBAO_TTS`:豆包 TTS,需填写 `url_template`、`api_key`(Token)、`model_name`(App ID)、`voice_id`(voice_type)。默认接口为 `https://openspeech.bytedance.com/api/v1/tts`,继承 HTTP TTS 队列和响应管线,内部固定按 `data -> base64_decode` 解析音频。 -- `OPENAI_TTS`:需填写 `url_template`、`api_key`、`model_name`、`voice_id`。 -- `VITS_TTS`:本地 VITS/Piper TTS。`tts_vits_package_path` 填本地模型包 `.zip` 或已解压目录,`tts_vits_speaker_id` 可选填数字 speaker id,`tts_vits_options` 可选填写 `sample_rate`、`threads`、`noise_scale`、`length_scale`、`noise_w`、`frontend`、`text_mode`、`speaker_count`、输入名和 blank/bos/eos token 等本地参数。 -3) STT(语音转文本)可选引擎: -- `SHERPA_NCNN`:本地识别,通常无需 API Key。 -- `OPENAI_STT`:需填写 `endpoint_url`、`api_key`、`model_name`。 -- `DEEPGRAM_STT`:需填写 `endpoint_url`、`api_key`、`model_name`。 -4) 最常见填错点(优先检查): -- `HTTP_TTS` 的 `headers` 不是合法 JSON(必须是对象)。 -- `HTTP_TTS` 的模板没放 `{text}` 占位符(GET 通常在 URL,POST 通常在 body)。 -- `HTTP_TTS` 的 `response_pipeline` 不是合法 JSON 数组,或步骤名 / `path` 填错。 -- `OPENAI_WS_TTS` 把 HTTP 地址填成了 WebSocket 地址,或把 WebSocket 地址误填成 HTTP 地址。 -- `VITS_TTS` 的 `tts_vits_package_path` 不是本地 `.zip` 模型包或已解压目录,或文件不存在。 -- `VITS_TTS` 模型包里没有可识别的 `.onnx` / config JSON / lexicon,或配置里缺少 `sample_rate` / token 映射。 -- `VITS_TTS` 的 `tts_vits_options` 不是合法 JSON(必须是对象),或把本地参数名 / 数值类型写错。 -- TTS/STT 的 endpoint 路径写错(比如把 chat/completions 写成 audio 接口)。 -- `model_name` 填了不存在的模型或与接口不匹配。 -- 改完配置后没有重新测试语音播报或语音识别。 -5) HTTP_TTS 占位符说明(按代码逻辑): -- 必填占位符:`{text}`。 - - 当 `http_method=GET` 时,`{text}` 必须在 `url_template` 里。 - - 当 `http_method=POST` 时,`{text}` 必须在 `request_body` 里。 -- 可选占位符:`{rate}`、`{pitch}`、`{voice}`。 -- 当前对外可稳定使用的占位符:`{text}`、`{rate}`、`{pitch}`、`{voice}`、`{apiKey}`、`{model}`、`{locale}`、`{uuid}`。 -6) HTTP_TTS 响应处理说明(已发布版本兼容): -- `response_pipeline` 留空或传 `[]`:保持旧行为,直接把首个响应体当音频。 -- 当服务端先返回 JSON、字段或下载链接时,再填写 `response_pipeline`,按步骤解析。 -- 当前可用步骤:`parse_json`、`pick`、`parse_json_string`、`http_get`、`http_request_from_object`、`base64_decode`。 -- 常见 JSON 下载链接场景: - - `response_pipeline`: `[{"type":"parse_json"},{"type":"pick","path":"audio_uri"},{"type":"http_get"}]` -- 若字段里还是 JSON 字符串: - - `response_pipeline`: `[{"type":"parse_json"},{"type":"pick","path":"data.payload"},{"type":"parse_json_string"},{"type":"pick","path":"audio.url"},{"type":"http_get"}]` -7) 可直接参考的最小模板: -- HTTP TTS(GET): - - `url_template`: `https://example.com/tts?text={text}` - - `headers`: `{}` - - `http_method`: `GET` - - `content_type`: `application/json` -- HTTP TTS(POST): - - `url_template`: `https://example.com/tts` - - `headers`: `{"Authorization":"Bearer "}` - - `http_method`: `POST` - - `content_type`: `application/json` - - `request_body`: `{"text":"{text}"}` -- OpenAI STT(默认常见): - - `endpoint_url`: `https://api.openai.com/v1/audio/transcriptions` - - `model_name`: `whisper-1` -8) 排查顺序(建议): -- 先确认选中的引擎类型是否正确(TTS 与 STT 分开看)。 -- 再检查 endpoint / key / model 三件套是否完整。 -- 再检查 HTTP 模板字段(headers JSON、method、body、占位符、response_pipeline)。 -- 至少做一次真实 TTS 播报测试;若还要排查 STT,再另外走语音识别链路。 -9) 对应工具(可直接用): -- `get_speech_services_config`:获取当前 TTS/STT 配置快照(含引擎类型与关键字段)。 -- `set_speech_services_config`:按字段修改 TTS/STT 配置(支持只改部分字段)。 -- `test_tts_playback`:按当前 TTS 配置播放一次测试文本(支持临时覆盖语速/音调)。 - -【多模态输入规则】 -1) 能力开关含义: -- 模型配置里的“支持 Tool Call / 识图 / 音频 / 视频”等开关,只是软件侧能力标识,不等于模型真实能力。 -- 实际配置时,必须依据模型真实支持情况来开关,不能乱开。 -2) 软件识图主链路: -- 当“对话功能模型”的模型配置开启识图且模型真实支持时:聊天附图会直接发给该模型识别。 -- 当对话模型不支持识图时:软件会尝试走 OCR,或走“识图功能模型”进行识图中转。 -3) 用户有识图需求时的可行条件: -- 条件 A:对话模型支持识图。 -- 条件 B:对话模型不支持识图,但识图功能模型支持识图。 -- 若识图功能模型也不支持识图:最终回退到 OCR。 - -【绘图输出说明】 -- 绘图通过工具包实现。 -- 软件内置了一些绘图包,可调用 list_sandbox_packages 查看。 -- 通常只需启用其中一个可用绘图包即可,无需全部开启。 - -【执行原则】 -- 严格按用户明确指示执行,不自行定义“问题”或追加未被要求的目标。 -- 任何会改动用户配置的操作(开关、导入/删除、写环境变量、模型配置增删改、重启)都必须先得到用户明确确认。 -- 用户没有明确要求执行某个工具时,不主动调用写入类工具。 -- 回答优先基于本手册的路径与规则,避免泛化推断。''' - en: '''Configuration troubleshooting guide. - -[Trigger conditions] -- The user mentions MCP/Skill install failure, startup failure, tools not appearing, import failure, duplicate name conflicts, or config editing -- The user mentions sandbox package toggles, built-in package listing, import/delete paths, or package enable-state issues -- The user asks to troubleshoot plugin config paths, deploy directories, enable switches, or environment variables -- The user mentions function model bindings, adding/deleting/updating model configs, or testing model connectivity -- The user says TTS/STT setup is confusing, too many fields to fill, or speech playback/recognition is not working -- The core issue is configuration/deployment flow rather than normal Q&A - -[MCP: install and troubleshooting] -1) Config directory: /sdcard/Download/Operit/mcp_plugins/ -- Main config: mcp_config.json -- Runtime status cache: server_status.json (non-realtime snapshot for status/tool cache only; not a troubleshooting source of truth) -2) Keep Android-side and Linux-side paths strictly separated: -- Android side is the source/import location, not the final runtime location -- Linux runtime directory is: ~/mcp_plugins/ -- Actual MCP startup always runs from the Linux side -3) Real deployment behavior (from implementation): -- Create target directory -- Copy plugin files from Android side to Linux side -- Execute auto-generated install/build commands (startup commands are skipped) -3.1) Local plugin recognition rule: -- This exists to handle nested storage layouts (for example `/-main/...`): the Android-side plugin directory must contain at least one marker file such as `README.md`, `package.json`, `mcp.config.json`, `main.js`, `main.py`, `index.js`, or `index.py`; otherwise it may be treated as not installed and never enter the startup list. -4) Command-based plugin handling: -- For command-based plugins using npx/uvx/uv, the system treats them as deployed and only performs minimal directory preparation. -- For Node-style command MCPs, keep the config in mcp_config.json aligned with upstream examples and still write `command: "npx"`; do not rewrite it to `pnpm` or `npm` yourself. -- When the app starts this kind of `npx` MCP, it internally rewrites `npx` to `pnpm dlx` and strips confirmation flags such as `-y` / `--yes`. -- That means the real runtime dependency for this kind of MCP is `pnpm`; `pnpm` must exist in the Linux terminal, and having only `npm` is not enough. -- Do not change the command to `npm`; this is known to trigger `double free` errors. The current convention is: configure it as `npx`, and let the app run it internally as `pnpm dlx`. -5) System startup behavior (critical): -- Local plugins: the app reads command/args/env from mcp_config.json and starts the process with cwd=~/mcp_plugins/; availability should be judged by real tool call/response -- Remote plugins: the app connects using endpoint + connectionType (optional bearerToken/headers) and verifies connectivity -6) Actual execution location of command (must be understood this way): -- For local plugins, the process starts in the Linux terminal environment with fixed working directory: ~/mcp_plugins/ -- The command from mcp_config.json is executed in that Linux working-directory context, not in the Android source directory -- Any relative paths in args are resolved against that cwd -6.1) node command example (by plugin ID): -- Example: pluginId=owner/my-plugin, so shortName=my-plugin and cwd=~/mcp_plugins/my-plugin -- If the entry file is ~/mcp_plugins/my-plugin/dist/index.js, write: - "command": "node" - "args": ["dist/index.js"] -- args must be relative to cwd; do not use Android paths such as /sdcard/... -7) mcp_config.json field rules: -- Top level must contain mcpServers (object) -- Each key in mcpServers is a serverId (normally keep it aligned with plugin ID) -- pluginId (the mcpServers key) only allows letters a-zA-Z, underscore (_), and spaces -- Common server fields: - - command (required, startup command) - - args (optional, string array) - - env (optional, key-value map) - - autoApprove (optional, array) - - disabled (optional, true means disabled) -- MCP environment variables must be configured in mcpServers..env; `read_environment_variable` / `write_environment_variable` do not read or write these MCP env entries. -8) Path-writing rules in mcp_config.json: -- Do not put Android absolute paths (for example /sdcard/...) in local plugin startup args -- Write local plugin command/args for Linux runtime; prefer relative paths because cwd is fixed to ~/mcp_plugins/ -- If an absolute path is required, it must be a Linux path, not an Android path -9) Enable switch: -- Local plugin uses mcpServers..disabled -- Remote plugin uses pluginMetadata..disabled -10) MCP troubleshooting order: -- Check enable switch -- Check local deployed directory exists and is non-empty -- Check whether command/args/env in mcp_config.json are complete -- Check whether args incorrectly use Android paths -- Check required key/token in env -- Do not treat `active` in server_status.json as the single source of truth; prioritize whether tools can be fetched/called successfully -- Check terminal dependencies (node/pnpm/python/uv) and MCP service status; Node-style `npx` MCPs actually depend on `pnpm`, so missing `pnpm` will prevent startup - -[Skill: install and troubleshooting] -1) Directory: /sdcard/Download/Operit/skills/ -2) Recognition rule: each Skill must be a folder containing SKILL.md (skill.md is also accepted). -3) How to add a skill (use this workflow): -- Download the skill from a trusted source (zip or repository source code). -- Extract it, then place the folder directly under /sdcard/Download/Operit/skills/ -- Final structure must be /sdcard/Download/Operit/skills// and that folder must contain SKILL.md -4) Metadata parsing: -- Prefer frontmatter name/description -- Fallback to name:/description: in the first 40 lines -5) AI visibility: -- If the list switch is off, the Skill remains local but is hidden from AI usage -6) Skill troubleshooting order: -- Verify path and SKILL.md -- Check duplicate-name conflict -- Check enable switch -- Check whether SKILL.md instructions are complete (steps/constraints/outputs) - -[Sandbox Package: install and troubleshooting] -1) External sandbox packages directory: -- Android/data/com.ai.assistance.operit/files/packages -2) Built-in packages: -- Built-in packages come from app bundled assets, not from the external directory above; usually manage via enable/disable instead of file deletion. -3) Import and delete: -- Import: place/import `.js` or `.toolpkg` into the external packages directory. -- Delete: for external packages, delete by file path after confirming dependency impact. -4) Toggle management (prefer tools): -- Call list_sandbox_packages first to get built-in + external package list and current enabled state. -- Then call set_sandbox_package_enabled(package_name, enabled) to apply changes. -5) Package authoring guide: -- https://cdn.jsdelivr.net/gh/AAswordman/Operit@main/docs/SCRIPT_DEV_SKILL.md -- This URL can be accessed directly with an HTTP GET request to fetch the raw Markdown document. -6) In-app debug install: -- For plain `.js` sandbox packages, prefer `debug_install_js_package`. -- For `ToolPkg`, prefer `debug_install_toolpkg`; it handles packaging/install flow for folder/manifest/.toolpkg sources and triggers the ToolPkg refresh path. - -[Package compatibility model (important)] -- The package list seen by AI is a unified abstraction, and each item may come from MCP, Skill, or Sandbox Package. -- Available package entries are not guaranteed to be a single type; mixed types are expected. -- `use_package` is tri-compatible and can be called uniformly for MCP/Skill/Sandbox Package. -- `ping_mcp` is a thin wrapper over `use_package` for quick package availability probing. - -[Market agent API (HTTP, read-only)] -- Base prefix: `https://api.operit.app/market-stats` -- Search: `/agent/search?q=&type=mcp|skill|package|script&limit=10` -- Detail: `/agent/items//` -- Install plan: `/agent/items///install-plan` -- `package` / `script` install_plan usually returns `download_url`, `tracked_download_url`, `sha256`, and `runtime_package_id`. -- `skill` install_plan usually returns `repository_url`. -- `mcp` install_plan may return `config` (usable as installConfig reference) or `repository_url`. -- The app does not currently expose a unified one-click market install API to JS; when installation is needed, download/extract/import by item type and use `debug_install_js_package`, `debug_install_toolpkg`, or `use_package` when appropriate. - -[Function model and model config] -1) Model config: -- Each model config is one full connection profile (provider / endpoint / api key / model_name / custom_headers / capability switches). -- Configs can be created, updated, and deleted; deleting the default `default` config is forbidden. -2) Function model binding: -- Each function type (for example CHAT/SUMMARY/TRANSLATION) is bound to one model config. -- If `model_name` contains multiple models (comma-separated), `model_index` selects which one to use. -3) Key tools (prefer these): -- `list_model_configs`: list all model configs and current function bindings. -- `create_model_config`: create a new model config (with optional initial provider/endpoint/key/model_name/custom_headers). -- `update_model_config`: update an existing config by config_id. -- `delete_model_config`: delete a config (default cannot be deleted). -- `list_function_model_configs`: list function -> config bindings only (lightweight). -- `get_function_model_config`: get one bound config detail by function type. -- `set_function_model_config`: assign config and model index for a function. -- `test_model_config_connection`: run settings-UI-equivalent connectivity/capability checks for a config. -4) After config changes: -- If a config is used by functions, corresponding function services are refreshed; binding updates also refresh target function service. -5) When users complain about truncated outputs: -- Use `get_function_model_config` to inspect `max_tokens` in the bound config for that function. -- For DeepSeek, a common default is 4096; enable `max_tokens_enabled` and try setting `max_tokens` to 8192. -- For other models, verify the supported output-token limit online before changing values. - -[Context summary module] -1) Purpose: -- Controls context window behavior and summary-trigger conditions to reduce context bloat, drift, and unstable long-chat responses. -- Only context-summary settings on the config currently bound to CHAT are effective in real conversation. -2) Core mechanism: -- There is a max switch in the app: `enable_max_context_mode`. -- When enabled, it uses `max_context_length`; when disabled, it uses `context_length`, so context length can be chosen dynamically to save tokens. -- `enable_summary` controls automatic summarization. When context exceeds `summary_token_threshold * current available context length`, summarization is triggered. -3) Recommended troubleshooting flow: -- Use `get_context_summary_config` to inspect current context-summary fields for a function. -- Use `set_context_summary_config` to set context-summary fields (you can override default values via params). -- If issues remain, fine-tune based on real model capability and workload. - -[TTS/STT speech-service configuration] -1) Where to configure: -- Configure it on the Speech Services page in app settings. -- The page auto-saves; after changes, speech-service instances are rebuilt automatically. -2) TTS (text-to-speech) engines: -- `SIMPLE_TTS`: system TTS, usually no network fields required. -- `HTTP_TTS`: mainly fill `url_template`, `headers`, `http_method`, `content_type`, `request_body`; if the service first returns JSON / fields / a download link, also fill `response_pipeline`. -- `OPENAI_WS_TTS`: fill `url_template`, `api_key`, `model_name`, `voice_id`. `url_template` should be a Realtime WebSocket endpoint such as `wss://api.openai.com/v1/realtime`. -- `SILICONFLOW_TTS`: fill `api_key`, `model_name`, `voice_id`. -- `MINIMAX_TTS`: fill `api_key`; optionally set `url_template`, `model_name`, and `voice_id`. Default endpoint is `https://api.minimaxi.com/v1/t2a_v2`, and audio is resolved from `data.audio` automatically. -- `MIMO_TTS`: fill `api_key`; optionally set `url_template`, `model_name`, and `voice_id`. Use `mimo-v2.5-tts` with a short `voice_id` for preset voices. Use `mimo-v2.5-tts-voiceclone` with a full `data:audio/...;base64,...` audio sample in `voice_id` for voice cloning. Default endpoint is `https://api.xiaomimimo.com/v1/chat/completions`, and audio is resolved from `choices[0].message.audio.data` with Base64 decoding. -- `DOUBAO_TTS`: Doubao TTS. Fill `url_template`, `api_key` (token), `model_name` (App ID), and `voice_id` (voice_type). Default endpoint is `https://openspeech.bytedance.com/api/v1/tts`; it inherits the HTTP TTS queue and response pipeline and resolves audio from `data` with Base64 decoding. -- `OPENAI_TTS`: fill `url_template`, `api_key`, `model_name`, `voice_id`. -- `VITS_TTS`: local VITS/Piper TTS. Set `tts_vits_package_path` to the local model package `.zip` or extracted package directory, optionally set `tts_vits_speaker_id` to a numeric speaker id, and use `tts_vits_options` for local options such as `sample_rate`, `threads`, `noise_scale`, `length_scale`, `noise_w`, `frontend`, `text_mode`, `speaker_count`, input names, and blank/bos/eos token settings. -3) STT (speech-to-text) engines: -- `SHERPA_NCNN`: local recognition, usually no API key required. -- `OPENAI_STT`: fill `endpoint_url`, `api_key`, `model_name`. -- `DEEPGRAM_STT`: fill `endpoint_url`, `api_key`, `model_name`. -4) Most common mistakes (check first): -- `headers` in `HTTP_TTS` is not valid JSON (must be an object). -- Missing `{text}` placeholder in HTTP TTS template (typically in URL for GET, in body for POST). -- `response_pipeline` in `HTTP_TTS` is not a valid JSON array, or a step name / `path` is incorrect. -- `OPENAI_WS_TTS` is configured with an HTTP URL instead of a WebSocket URL, or vice versa. -- `tts_vits_package_path` in `VITS_TTS` is not a local `.zip` model package or extracted package directory, or it does not exist. -- The `VITS_TTS` package has no recognizable `.onnx` / config JSON / lexicon, or the config is missing `sample_rate` / token mappings. -- `tts_vits_options` in `VITS_TTS` is not valid JSON, or local option names / numeric values are invalid. -- Wrong endpoint path for TTS/STT (for example using chat/completions instead of audio endpoints). -- `model_name` does not exist or does not match the API. -- No real retest after saving config. -5) HTTP_TTS placeholder rules (from implementation): -- Required placeholder: `{text}`. - - When `http_method=GET`, `{text}` must appear in `url_template`. - - When `http_method=POST`, `{text}` must appear in `request_body`. -- Optional placeholders: `{rate}`, `{pitch}`, `{voice}`. -- Stable placeholders for external configuration: `{text}`, `{rate}`, `{pitch}`, `{voice}`, `{apiKey}`, `{model}`, `{locale}`, `{uuid}`. -6) HTTP_TTS response handling notes (forward-compatible with released configs): -- Leave `response_pipeline` empty or use `[]` to keep the old behavior: the first response body is treated as audio directly. -- Only fill `response_pipeline` when the service returns JSON, nested fields, or a follow-up download URL before audio is available. -- Currently supported steps: `parse_json`, `pick`, `parse_json_string`, `http_get`, `http_request_from_object`, `base64_decode`. -- Common JSON download-link case: - - `response_pipeline`: `[{"type":"parse_json"},{"type":"pick","path":"audio_uri"},{"type":"http_get"}]` -- If the picked field is itself a JSON string: - - `response_pipeline`: `[{"type":"parse_json"},{"type":"pick","path":"data.payload"},{"type":"parse_json_string"},{"type":"pick","path":"audio.url"},{"type":"http_get"}]` -7) Minimal templates you can copy: -- HTTP TTS (GET): - - `url_template`: `https://example.com/tts?text={text}` - - `headers`: `{}` - - `http_method`: `GET` - - `content_type`: `application/json` -- HTTP TTS (POST): - - `url_template`: `https://example.com/tts` - - `headers`: `{"Authorization":"Bearer "}` - - `http_method`: `POST` - - `content_type`: `application/json` - - `request_body`: `{"text":"{text}"}` -- OpenAI STT (common default): - - `endpoint_url`: `https://api.openai.com/v1/audio/transcriptions` - - `model_name`: `whisper-1` -8) Recommended troubleshooting order: -- Confirm the selected engine type first (TTS and STT separately). -- Check endpoint/key/model as a bundle. -- Then verify HTTP template fields (headers JSON, method, body, placeholder, response_pipeline). -- Run at least one real TTS playback test; if STT also needs troubleshooting, verify recognition in a separate speech flow. -9) Related tools: -- `get_speech_services_config`: fetch current TTS/STT config snapshot (engine types + key fields). -- `set_speech_services_config`: update TTS/STT config fields (partial update supported). -- `test_tts_playback`: play one test utterance with the current TTS config (supports temporary rate/pitch overrides). - -[Multimodal input rules] -1) Meaning of capability switches: -- Switches like Tool Call / image / audio / video in model config are software-side capability markers, not proof of real model capability. -- Configure these switches according to actual model support; do not enable blindly. -2) Image understanding main path in app: -- If the chat-function model config enables image understanding and the model truly supports it, attached images are sent directly to that chat model. -- If the chat model does not support image understanding, the app will try OCR, or use the dedicated image-recognition function model as a relay. -3) Valid conditions when users need image understanding: -- Condition A: chat model supports image understanding. -- Condition B: chat model does not support it, but image-recognition function model supports it. -- If the image-recognition model also does not support it, the final fallback is OCR. - -[Drawing output note] -- Drawing is implemented via package tools. -- The app includes several built-in drawing packages; use list_sandbox_packages to inspect them. -- Usually enabling one available drawing package is enough; no need to enable all of them. - -[Execution principles] -- Follow the user's explicit instructions strictly; do not define your own "problem" or add unrequested goals. -- Any configuration-changing action (toggle/import/delete/write env/model config CRUD/restart) requires explicit user confirmation first. -- If the user did not explicitly ask to execute a specific tool, do not proactively run write-type tools. -- Answer with concrete paths/rules from this guide and avoid generic assumptions.''' - } - parameters: [] - advice: true - }, - { - name: "how_make_skill" - description: { - zh: '''返回如何制作 skill 的双语说明。''' - en: '''Return a bilingual guide for creating a skill.''' - } - parameters: [] - }, - { - name: "list_sandbox_packages" - description: { - zh: '''获取沙盒包列表(内置+外部)及当前启用状态、管理路径。''' - en: '''Get sandbox package list (built-in + external), current enabled states, and management paths.''' - } - parameters: [] - }, - { - name: "set_sandbox_package_enabled" - description: { - zh: '''设置沙盒包开关状态。''' - en: '''Set sandbox package enabled state.''' - } - parameters: [ - { - name: "package_name" - description: { - zh: "沙盒包名称" - en: "Sandbox package name" - } - type: string - required: true - }, - { - name: "enabled" - description: { - zh: "是否启用(true/false)" - en: "Enable state (true/false)" - } - type: boolean - required: true - } - ] - }, - { - name: "debug_install_js_package" - description: { - zh: '''将 Android 侧的普通 `.js` 沙盒包直接烧录到外部 packages 目录,并刷新、启用、重新加载,便于在软件内调试。''' - en: '''Install an Android-side plain `.js` sandbox package into the external packages directory, refresh it, enable it, and reload it for in-app debugging.''' - } - parameters: [ - { - name: "source_path" - description: { - zh: "Android 侧 `.js` 源文件路径" - en: "Android-side `.js` source file path" - } - type: string - required: true - }, - { - name: "enable_after_install" - description: { - zh: "安装后是否自动启用,默认 true" - en: "Whether to enable it automatically after install, default true" - } - type: boolean - required: false - }, - { - name: "activate_after_install" - description: { - zh: "安装后是否自动 use_package 重新加载,默认 true" - en: "Whether to automatically call use_package after install, default true" - } - type: boolean - required: false - } - ] - }, - { - name: "debug_install_toolpkg" - description: { - zh: '''根据 Android 侧的 ToolPkg 目录、manifest 或现成 `.toolpkg`,直接打包/烧录到外部 packages 目录,并触发 ToolPkg 安装与刷新链路。''' - en: '''Package or install an Android-side ToolPkg folder, manifest, or existing `.toolpkg` into the external packages directory and trigger the ToolPkg install/refresh flow.''' - } - parameters: [ - { - name: "source_path" - description: { - zh: "Android 侧 ToolPkg 目录、manifest.json/manifest.hjson 或 `.toolpkg` 路径" - en: "Android-side ToolPkg folder, manifest.json/manifest.hjson, or `.toolpkg` path" - } - type: string - required: true - }, - { - name: "reset_subpackage_states" - description: { - zh: "是否按 manifest 默认值重置子包启用状态,默认 true" - en: "Whether to reset subpackage enable states from manifest defaults, default true" - } - type: boolean - required: false - }, - { - name: "activate_subpackages" - description: { - zh: "可选,逗号或换行分隔的子包 ID;安装后会自动启用并 use_package" - en: "Optional comma/newline separated subpackage IDs; they will be enabled and activated after install" - } - type: string - required: false - }, - { - name: "wait_ms" - description: { - zh: "安装广播后等待并轮询刷新的毫秒数,默认 1500" - en: "Milliseconds to wait and poll for refresh after sending the install broadcast, default 1500" - } - type: integer - required: false - } - ] - }, - { - name: "debug_run_sandbox_script" - description: { - zh: '''直接运行一段 sandbox script。可传 Android 侧 `source_path`,也可直接传 `source_code` 内联代码;会返回结构化执行结果与日志事件。''' - en: '''Run a sandbox script directly. Accepts either an Android-side `source_path` or inline `source_code`, and returns structured execution results with log events.''' - } - parameters: [ - { - name: "source_path" - description: { - zh: "Android 侧脚本文件路径;与 source_code 二选一" - en: "Android-side script file path; use either this or source_code" - } - type: string - required: false - }, - { - name: "source_code" - description: { - zh: "直接执行的内联 JavaScript 代码;与 source_path 二选一" - en: "Inline JavaScript code to execute directly; use either this or source_path" - } - type: string - required: false - }, - { - name: "params_json" - description: { - zh: "传给脚本运行时的 JSON 参数字符串,默认 {}" - en: "JSON parameter string passed to the script runtime, default {}" - } - type: string - required: false - }, - { - name: "env_file_path" - description: { - zh: "可选,Android 侧 env 文件路径" - en: "Optional Android-side env file path" - } - type: string - required: false - }, - { - name: "script_label" - description: { - zh: "可选,仅用于内联代码模式下生成结果文件名和显示标识" - en: "Optional label used only for inline-code mode to name the result file and display path" - } - type: string - required: false - }, - { - name: "wait_ms" - description: { - zh: "等待结构化结果文件的毫秒数,默认 15000" - en: "Milliseconds to wait for the structured result file, default 15000" - } - type: integer - required: false - } - ] - }, - { - name: "read_environment_variable" - description: { - zh: '''读取指定环境变量当前值(仅用于沙盒包脚本环境变量排查,不用于 MCP 配置)。''' - en: '''Read current value of a specified environment variable (sandbox-package script env troubleshooting only, not MCP config env).''' - } - parameters: [ - { - name: "key" - description: { - zh: "环境变量名" - en: "Environment variable key" - } - type: string - required: true - } - ] - }, - { - name: "write_environment_variable" - description: { - zh: '''写入指定环境变量;value 为空时会清除该变量(仅用于沙盒包脚本环境变量,不用于 MCP 配置 env)。''' - en: '''Write a specified environment variable; empty value clears it (sandbox-package script env only, not MCP config env).''' - } - parameters: [ - { - name: "key" - description: { - zh: "环境变量名" - en: "Environment variable key" - } - type: string - required: true - }, - { - name: "value" - description: { - zh: "变量值;为空时清除该变量" - en: "Variable value; empty clears the variable" - } - type: string - required: false - } - ] - }, - { - name: "restart_mcp_with_logs" - description: { - zh: '''触发一次 MCP 重启流程,返回每个插件的启动日志与状态摘要。''' - en: '''Trigger one MCP restart flow and return per-plugin startup logs with status summary.''' - } - parameters: [ - { - name: "timeout_ms" - description: { - zh: "可选,最大等待时长(毫秒)" - en: "Optional max wait time in milliseconds" - } - type: integer - required: false - } - ] - }, - { - name: "get_speech_services_config" - description: { - zh: '''获取当前 TTS/STT 语音服务配置快照。''' - en: '''Get current TTS/STT speech services config snapshot.''' - } - parameters: [] - }, - { - name: "set_speech_services_config" - description: { - zh: '''按字段更新 TTS/STT 语音服务配置(支持部分字段更新)。''' - en: '''Update TTS/STT speech services config by fields (partial update supported).''' - } - parameters: [ - { - name: "tts_service_type" - description: { - zh: "可选,SIMPLE_TTS/HTTP_TTS/OPENAI_WS_TTS/SILICONFLOW_TTS/MINIMAX_TTS/MIMO_TTS/DOUBAO_TTS/OPENAI_TTS/VITS_TTS" - en: "Optional, SIMPLE_TTS/HTTP_TTS/OPENAI_WS_TTS/SILICONFLOW_TTS/MINIMAX_TTS/MIMO_TTS/DOUBAO_TTS/OPENAI_TTS/VITS_TTS" - } - type: string - required: false - }, - { - name: "tts_url_template" - description: { - zh: "可选,HTTP 类 TTS 的 URL 模板。HTTP 系列仅支持 `{text}`、`{rate}`、`{pitch}`、`{voice}`" - en: "Optional URL template for HTTP-style TTS providers. HTTP-style providers support only `{text}`, `{rate}`, `{pitch}`, `{voice}`" - } - type: string - required: false - }, - { - name: "tts_api_key" - description: { - zh: "可选,TTS API Key" - en: "Optional TTS API key" - } - type: string - required: false - }, - { - name: "tts_headers" - description: { - zh: "可选,HTTP 类 TTS headers 的 JSON 对象字符串" - en: "Optional JSON object string for HTTP-style TTS headers" - } - type: string - required: false - }, - { - name: "tts_http_method" - description: { - zh: "可选,GET/POST" - en: "Optional, GET/POST" - } - type: string - required: false - }, - { - name: "tts_request_body" - description: { - zh: "可选,HTTP 类 TTS 的 POST body 模板。仅支持 `{text}`、`{rate}`、`{pitch}`、`{voice}`" - en: "Optional POST body template for HTTP-style TTS providers. Supports only `{text}`, `{rate}`, `{pitch}`, `{voice}`" - } - type: string - required: false - }, - { - name: "tts_content_type" - description: { - zh: "可选,TTS Content-Type" - en: "Optional TTS content type" - } - type: string - required: false - }, - { - name: "tts_locale" - description: { - zh: "可选,TTS 语言标签,例如 zh-CN 或 en-US" - en: "Optional TTS locale tag, for example zh-CN or en-US" - } - type: string - required: false - }, - { - name: "tts_voice_id" - description: { - zh: "可选,TTS 音色 ID。MIMO voiceclone 可填写完整 data:audio/...;base64,... 音频样本" - en: "Optional TTS voice id. For MIMO voiceclone, this may be the full data:audio/...;base64,... audio sample" - } - type: string - required: false - }, - { - name: "tts_model_name" - description: { - zh: "可选,TTS 模型名" - en: "Optional TTS model name" - } - type: string - required: false - }, - { - name: "tts_vits_package_path" - description: { - zh: "可选,本地 VITS/Piper TTS 模型包路径,支持 .zip 文件或已解压目录" - en: "Optional local VITS/Piper TTS package path, supporting a .zip file or extracted package directory" - } - type: string - required: false - }, - { - name: "tts_vits_speaker_id" - description: { - zh: "可选,VITS/Piper TTS 模型包需要的数字 speaker id" - en: "Optional numeric speaker id required by the VITS/Piper TTS package" - } - type: string - required: false - }, - { - name: "tts_vits_options" - description: { - zh: "可选,VITS/Piper TTS 模型包参数 JSON 对象字符串" - en: "Optional JSON object string for VITS/Piper TTS package options" - } - type: string - required: false - }, - { - name: "tts_response_pipeline" - description: { - zh: "可选,HTTP TTS 响应处理管线 JSON 数组字符串。留空或 `[]` 时保持旧行为,直接把响应体当音频" - en: "Optional HTTP TTS response pipeline JSON array string. Leave empty or use `[]` to keep the old direct-audio behavior" - } - type: string - required: false - }, - { - name: "tts_cleaner_regexs" - description: { - zh: "可选,TTS 清理正则列表 JSON 数组字符串" - en: "Optional JSON array string for TTS cleaner regex list" - } - type: string - required: false - }, - { - name: "tts_speech_rate" - description: { - zh: "可选,TTS 语速" - en: "Optional TTS speech rate" - } - type: number - required: false - }, - { - name: "tts_pitch" - description: { - zh: "可选,TTS 音调" - en: "Optional TTS pitch" - } - type: number - required: false - }, - { - name: "stt_service_type" - description: { - zh: "可选,SHERPA_NCNN/OPENAI_STT/DEEPGRAM_STT" - en: "Optional, SHERPA_NCNN/OPENAI_STT/DEEPGRAM_STT" - } - type: string - required: false - }, - { - name: "stt_endpoint_url" - description: { - zh: "可选,STT endpoint URL" - en: "Optional STT endpoint URL" - } - type: string - required: false - }, - { - name: "stt_api_key" - description: { - zh: "可选,STT API Key" - en: "Optional STT API key" - } - type: string - required: false - }, - { - name: "stt_model_name" - description: { - zh: "可选,STT 模型名" - en: "Optional STT model name" - } - type: string - required: false - } - ] - }, - { - name: "test_tts_playback" - description: { - zh: '''按当前 TTS 配置播放一次测试文本。''' - en: '''Play one TTS test utterance with the current configuration.''' - } - parameters: [ - { - name: "text" - description: { - zh: "必填,要播放的测试文本" - en: "Required test text to play" - } - type: string - required: true - }, - { - name: "interrupt" - description: { - zh: "可选,播放前是否先中断当前播报" - en: "Optional, interrupt current playback before this test" - } - type: boolean - required: false - }, - { - name: "speech_rate" - description: { - zh: "可选,仅本次测试生效的语速覆盖值" - en: "Optional speech-rate override for this test only" - } - type: number - required: false - }, - { - name: "pitch" - description: { - zh: "可选,仅本次测试生效的音调覆盖值" - en: "Optional pitch override for this test only" - } - type: number - required: false - } - ] - }, - { - name: "list_model_configs" - description: { - zh: '''列出全部模型配置及功能模型绑定关系。''' - en: '''List all model configs and function-model bindings.''' - } - parameters: [] - }, - { - name: "create_model_config" - description: { - zh: '''新增模型配置(可带初始化字段)。''' - en: '''Create a model config (optional initialization fields).''' - } - parameters: [ - { - name: "name" - description: { - zh: "可选,配置名称" - en: "Optional config name" - } - type: string - required: false - }, - { - name: "api_provider_type" - description: { - zh: "可选,提供商枚举名(如 OPENAI_GENERIC/OPENAI_LOCAL/OPENAI_RESPONSES_GENERIC/DEEPSEEK/GEMINI_GENERIC/LMSTUDIO/OLLAMA/MNN/LLAMA_CPP;其中 LMSTUDIO/OLLAMA/OPENAI_LOCAL/MNN/LLAMA_CPP 为本地模型链路)" - en: "Optional provider enum name (e.g. OPENAI_GENERIC/OPENAI_LOCAL/OPENAI_RESPONSES_GENERIC/DEEPSEEK/GEMINI_GENERIC/LMSTUDIO/OLLAMA/MNN/LLAMA_CPP; LMSTUDIO/OLLAMA/OPENAI_LOCAL/MNN/LLAMA_CPP are local-model providers)" - } - type: string - required: false - }, - { - name: "api_endpoint" - description: { - zh: "可选,API端点" - en: "Optional API endpoint" - } - type: string - required: false - }, - { - name: "api_key" - description: { - zh: "可选,API Key" - en: "Optional API key" - } - type: string - required: false - }, - { - name: "model_name" - description: { - zh: "可选,模型名(多个可逗号分隔)" - en: "Optional model name (comma-separated for multiple models)" - } - type: string - required: false - }, - { - name: "max_tokens_enabled" - description: { - zh: "可选,是否启用 max_tokens 参数" - en: "Optional switch for max_tokens" - } - type: boolean - required: false - }, - { - name: "max_tokens" - description: { - zh: "可选,max_tokens 数值" - en: "Optional max_tokens value" - } - type: integer - required: false - }, - { - name: "temperature_enabled" - description: { - zh: "可选,是否启用 temperature 参数" - en: "Optional switch for temperature" - } - type: boolean - required: false - }, - { - name: "temperature" - description: { - zh: "可选,temperature 数值" - en: "Optional temperature value" - } - type: number - required: false - }, - { - name: "top_p_enabled" - description: { - zh: "可选,是否启用 top_p 参数" - en: "Optional switch for top_p" - } - type: boolean - required: false - }, - { - name: "top_p" - description: { - zh: "可选,top_p 数值" - en: "Optional top_p value" - } - type: number - required: false - }, - { - name: "top_k_enabled" - description: { - zh: "可选,是否启用 top_k 参数" - en: "Optional switch for top_k" - } - type: boolean - required: false - }, - { - name: "top_k" - description: { - zh: "可选,top_k 数值" - en: "Optional top_k value" - } - type: integer - required: false - }, - { - name: "presence_penalty_enabled" - description: { - zh: "可选,是否启用 presence_penalty 参数" - en: "Optional switch for presence_penalty" - } - type: boolean - required: false - }, - { - name: "presence_penalty" - description: { - zh: "可选,presence_penalty 数值" - en: "Optional presence_penalty value" - } - type: number - required: false - }, - { - name: "frequency_penalty_enabled" - description: { - zh: "可选,是否启用 frequency_penalty 参数" - en: "Optional switch for frequency_penalty" - } - type: boolean - required: false - }, - { - name: "frequency_penalty" - description: { - zh: "可选,frequency_penalty 数值" - en: "Optional frequency_penalty value" - } - type: number - required: false - }, - { - name: "repetition_penalty_enabled" - description: { - zh: "可选,是否启用 repetition_penalty 参数" - en: "Optional switch for repetition_penalty" - } - type: boolean - required: false - }, - { - name: "repetition_penalty" - description: { - zh: "可选,repetition_penalty 数值" - en: "Optional repetition_penalty value" - } - type: number - required: false - }, - { - name: "custom_parameters" - description: { - zh: "可选,自定义参数 JSON 字符串" - en: "Optional custom parameters JSON string" - } - type: string - required: false - }, - { - name: "custom_headers" - description: { - zh: "可选,自定义请求头 JSON 对象字符串" - en: "Optional custom request headers JSON object string" - } - type: string - required: false - }, - { - name: "context_length" - description: { - zh: "可选,上下文长度倍率" - en: "Optional context length multiplier" - } - type: number - required: false - }, - { - name: "max_context_length" - description: { - zh: "可选,最大上下文长度倍率" - en: "Optional max context length multiplier" - } - type: number - required: false - }, - { - name: "enable_max_context_mode" - description: { - zh: "可选,是否启用最大上下文模式" - en: "Optional max-context mode switch" - } - type: boolean - required: false - }, - { - name: "summary_token_threshold" - description: { - zh: "可选,总结触发阈值" - en: "Optional summary trigger threshold" - } - type: number - required: false - }, - { - name: "enable_summary" - description: { - zh: "可选,是否启用总结" - en: "Optional summary switch" - } - type: boolean - required: false - }, - { - name: "enable_summary_by_message_count" - description: { - zh: "可选,是否按消息数触发总结" - en: "Optional summary-by-message-count switch" - } - type: boolean - required: false - }, - { - name: "summary_message_count_threshold" - description: { - zh: "可选,按消息数总结的阈值" - en: "Optional message-count threshold for summary" - } - type: integer - required: false - }, - { - name: "enable_direct_image_processing" - description: { - zh: "可选,是否启用直接图片处理" - en: "Optional direct image processing switch" - } - type: boolean - required: false - }, - { - name: "enable_direct_audio_processing" - description: { - zh: "可选,是否启用直接音频处理" - en: "Optional direct audio processing switch" - } - type: boolean - required: false - }, - { - name: "enable_direct_video_processing" - description: { - zh: "可选,是否启用直接视频处理" - en: "Optional direct video processing switch" - } - type: boolean - required: false - }, - { - name: "enable_google_search" - description: { - zh: "可选,是否启用 Google Search" - en: "Optional Google Search switch" - } - type: boolean - required: false - }, - { - name: "enable_claude_1h_prompt_cache" - description: { - zh: "可选,是否启用 Claude 1h Prompt Cache" - en: "Optional Claude 1h prompt cache switch" - } - type: boolean - required: false - }, - { - name: "enable_tool_call" - description: { - zh: "可选,是否开启Tool Call" - en: "Optional tool-call switch" - } - type: boolean - required: false - }, - { - name: "mnn_forward_type" - description: { - zh: "可选,MNN 前向类型" - en: "Optional MNN forward type" - } - type: integer - required: false - }, - { - name: "mnn_thread_count" - description: { - zh: "可选,MNN 线程数" - en: "Optional MNN thread count" - } - type: integer - required: false - }, - { - name: "llama_thread_count" - description: { - zh: "可选,llama.cpp 线程数" - en: "Optional llama.cpp thread count" - } - type: integer - required: false - }, - { - name: "llama_context_size" - description: { - zh: "可选,llama.cpp 上下文长度" - en: "Optional llama.cpp context size" - } - type: integer - required: false - }, - { - name: "llama_gpu_layers" - description: { - zh: "可选,llama.cpp GPU 层数" - en: "Optional llama.cpp GPU layer count" - } - type: integer - required: false - }, - { - name: "request_limit_per_minute" - description: { - zh: "可选,每分钟请求限制" - en: "Optional request-per-minute limit" - } - type: integer - required: false - }, - { - name: "max_concurrent_requests" - description: { - zh: "可选,最大并发请求数" - en: "Optional max concurrent requests" - } - type: integer - required: false - } - ] - }, - { - name: "update_model_config" - description: { - zh: '''按 config_id 修改模型配置。''' - en: '''Update model config by config_id.''' - } - parameters: [ - { - name: "config_id" - description: { - zh: "目标配置ID" - en: "Target config id" - } - type: string - required: true - }, - { - name: "name" - description: { - zh: "可选,配置名称" - en: "Optional config name" - } - type: string - required: false - }, - { - name: "api_provider_type" - description: { - zh: "可选,提供商枚举名(如 OPENAI_GENERIC/OPENAI_LOCAL/OPENAI_RESPONSES_GENERIC/DEEPSEEK/GEMINI_GENERIC/LMSTUDIO/OLLAMA/MNN/LLAMA_CPP;其中 LMSTUDIO/OLLAMA/OPENAI_LOCAL/MNN/LLAMA_CPP 为本地模型链路)" - en: "Optional provider enum name (e.g. OPENAI_GENERIC/OPENAI_LOCAL/OPENAI_RESPONSES_GENERIC/DEEPSEEK/GEMINI_GENERIC/LMSTUDIO/OLLAMA/MNN/LLAMA_CPP; LMSTUDIO/OLLAMA/OPENAI_LOCAL/MNN/LLAMA_CPP are local-model providers)" - } - type: string - required: false - }, - { - name: "api_endpoint" - description: { - zh: "可选,API端点" - en: "Optional API endpoint" - } - type: string - required: false - }, - { - name: "api_key" - description: { - zh: "可选,API Key" - en: "Optional API key" - } - type: string - required: false - }, - { - name: "model_name" - description: { - zh: "可选,模型名(多个可逗号分隔)" - en: "Optional model name (comma-separated for multiple models)" - } - type: string - required: false - }, - { - name: "max_tokens_enabled" - description: { - zh: "可选,是否启用 max_tokens 参数" - en: "Optional switch for max_tokens" - } - type: boolean - required: false - }, - { - name: "max_tokens" - description: { - zh: "可选,max_tokens 数值" - en: "Optional max_tokens value" - } - type: integer - required: false - }, - { - name: "temperature_enabled" - description: { - zh: "可选,是否启用 temperature 参数" - en: "Optional switch for temperature" - } - type: boolean - required: false - }, - { - name: "temperature" - description: { - zh: "可选,temperature 数值" - en: "Optional temperature value" - } - type: number - required: false - }, - { - name: "top_p_enabled" - description: { - zh: "可选,是否启用 top_p 参数" - en: "Optional switch for top_p" - } - type: boolean - required: false - }, - { - name: "top_p" - description: { - zh: "可选,top_p 数值" - en: "Optional top_p value" - } - type: number - required: false - }, - { - name: "top_k_enabled" - description: { - zh: "可选,是否启用 top_k 参数" - en: "Optional switch for top_k" - } - type: boolean - required: false - }, - { - name: "top_k" - description: { - zh: "可选,top_k 数值" - en: "Optional top_k value" - } - type: integer - required: false - }, - { - name: "presence_penalty_enabled" - description: { - zh: "可选,是否启用 presence_penalty 参数" - en: "Optional switch for presence_penalty" - } - type: boolean - required: false - }, - { - name: "presence_penalty" - description: { - zh: "可选,presence_penalty 数值" - en: "Optional presence_penalty value" - } - type: number - required: false - }, - { - name: "frequency_penalty_enabled" - description: { - zh: "可选,是否启用 frequency_penalty 参数" - en: "Optional switch for frequency_penalty" - } - type: boolean - required: false - }, - { - name: "frequency_penalty" - description: { - zh: "可选,frequency_penalty 数值" - en: "Optional frequency_penalty value" - } - type: number - required: false - }, - { - name: "repetition_penalty_enabled" - description: { - zh: "可选,是否启用 repetition_penalty 参数" - en: "Optional switch for repetition_penalty" - } - type: boolean - required: false - }, - { - name: "repetition_penalty" - description: { - zh: "可选,repetition_penalty 数值" - en: "Optional repetition_penalty value" - } - type: number - required: false - }, - { - name: "custom_parameters" - description: { - zh: "可选,自定义参数 JSON 字符串" - en: "Optional custom parameters JSON string" - } - type: string - required: false - }, - { - name: "custom_headers" - description: { - zh: "可选,自定义请求头 JSON 对象字符串" - en: "Optional custom request headers JSON object string" - } - type: string - required: false - }, - { - name: "context_length" - description: { - zh: "可选,上下文长度倍率" - en: "Optional context length multiplier" - } - type: number - required: false - }, - { - name: "max_context_length" - description: { - zh: "可选,最大上下文长度倍率" - en: "Optional max context length multiplier" - } - type: number - required: false - }, - { - name: "enable_max_context_mode" - description: { - zh: "可选,是否启用最大上下文模式" - en: "Optional max-context mode switch" - } - type: boolean - required: false - }, - { - name: "summary_token_threshold" - description: { - zh: "可选,总结触发阈值" - en: "Optional summary trigger threshold" - } - type: number - required: false - }, - { - name: "enable_summary" - description: { - zh: "可选,是否启用总结" - en: "Optional summary switch" - } - type: boolean - required: false - }, - { - name: "enable_summary_by_message_count" - description: { - zh: "可选,是否按消息数触发总结" - en: "Optional summary-by-message-count switch" - } - type: boolean - required: false - }, - { - name: "summary_message_count_threshold" - description: { - zh: "可选,按消息数总结的阈值" - en: "Optional message-count threshold for summary" - } - type: integer - required: false - }, - { - name: "enable_direct_image_processing" - description: { - zh: "可选,是否启用直接图片处理" - en: "Optional direct image processing switch" - } - type: boolean - required: false - }, - { - name: "enable_direct_audio_processing" - description: { - zh: "可选,是否启用直接音频处理" - en: "Optional direct audio processing switch" - } - type: boolean - required: false - }, - { - name: "enable_direct_video_processing" - description: { - zh: "可选,是否启用直接视频处理" - en: "Optional direct video processing switch" - } - type: boolean - required: false - }, - { - name: "enable_google_search" - description: { - zh: "可选,是否启用 Google Search" - en: "Optional Google Search switch" - } - type: boolean - required: false - }, - { - name: "enable_claude_1h_prompt_cache" - description: { - zh: "可选,是否启用 Claude 1h Prompt Cache" - en: "Optional Claude 1h prompt cache switch" - } - type: boolean - required: false - }, - { - name: "enable_tool_call" - description: { - zh: "可选,是否开启Tool Call" - en: "Optional tool-call switch" - } - type: boolean - required: false - }, - { - name: "mnn_forward_type" - description: { - zh: "可选,MNN 前向类型" - en: "Optional MNN forward type" - } - type: integer - required: false - }, - { - name: "mnn_thread_count" - description: { - zh: "可选,MNN 线程数" - en: "Optional MNN thread count" - } - type: integer - required: false - }, - { - name: "llama_thread_count" - description: { - zh: "可选,llama.cpp 线程数" - en: "Optional llama.cpp thread count" - } - type: integer - required: false - }, - { - name: "llama_context_size" - description: { - zh: "可选,llama.cpp 上下文长度" - en: "Optional llama.cpp context size" - } - type: integer - required: false - }, - { - name: "llama_gpu_layers" - description: { - zh: "可选,llama.cpp GPU 层数" - en: "Optional llama.cpp GPU layer count" - } - type: integer - required: false - }, - { - name: "request_limit_per_minute" - description: { - zh: "可选,每分钟请求限制" - en: "Optional request-per-minute limit" - } - type: integer - required: false - }, - { - name: "max_concurrent_requests" - description: { - zh: "可选,最大并发请求数" - en: "Optional max concurrent requests" - } - type: integer - required: false - } - ] - }, - { - name: "delete_model_config" - description: { - zh: '''按 config_id 删除模型配置(默认配置不可删)。''' - en: '''Delete model config by config_id (default cannot be deleted).''' - } - parameters: [ - { - name: "config_id" - description: { - zh: "目标配置ID" - en: "Target config id" - } - type: string - required: true - } - ] - }, - { - name: "list_function_model_configs" - description: { - zh: '''仅列出功能模型绑定关系(功能 -> 配置 + 模型索引)。''' - en: '''List function model bindings only (function -> config + model index).''' - } - parameters: [] - }, - { - name: "get_function_model_config" - description: { - zh: '''查看某个功能当前绑定的单个模型配置详情。''' - en: '''Get one function's currently bound model config detail.''' - } - parameters: [ - { - name: "function_type" - description: { - zh: "功能类型枚举名" - en: "Function type enum name" - } - type: string - required: true - } - ] - }, - { - name: "get_context_summary_config" - description: { - zh: '''获取某个功能当前绑定模型配置中的上下文总结参数。''' - en: '''Get context-summary settings from the model config bound to a function.''' - } - parameters: [ - { - name: "function_type" - description: { - zh: "可选,功能类型;默认 CHAT" - en: "Optional function type; default CHAT" - } - type: string - required: false - } - ] - }, - { - name: "set_context_summary_config" - description: { - zh: '''为某个功能绑定配置设置上下文总结参数(可选覆盖默认值)。''' - en: '''Set context-summary settings for a function binding (optional overrides supported).''' - } - parameters: [ - { - name: "function_type" - description: { - zh: "可选,功能类型;默认 CHAT" - en: "Optional function type; default CHAT" - } - type: string - required: false - }, - { - name: "context_length" - description: { - zh: "可选,基础上下文长度" - en: "Optional base context length" - } - type: number - required: false - }, - { - name: "max_context_length" - description: { - zh: "可选,最大上下文长度" - en: "Optional max context length" - } - type: number - required: false - }, - { - name: "enable_max_context_mode" - description: { - zh: "可选,是否启用最大上下文模式" - en: "Optional max-context-mode switch" - } - type: boolean - required: false - }, - { - name: "summary_token_threshold" - description: { - zh: "可选,总结触发 token 阈值(0~1)" - en: "Optional token-ratio threshold for summary trigger (0~1)" - } - type: number - required: false - }, - { - name: "enable_summary" - description: { - zh: "可选,是否启用上下文总结" - en: "Optional context-summary switch" - } - type: boolean - required: false - }, - { - name: "enable_summary_by_message_count" - description: { - zh: "可选,是否启用按消息条数触发总结" - en: "Optional message-count summary trigger switch" - } - type: boolean - required: false - }, - { - name: "summary_message_count_threshold" - description: { - zh: "可选,按消息条数触发总结阈值" - en: "Optional message-count threshold for summary trigger" - } - type: integer - required: false - } - ] - }, - { - name: "set_function_model_config" - description: { - zh: '''为功能指定模型配置与模型索引。''' - en: '''Set model config and model index for a function.''' - } - parameters: [ - { - name: "function_type" - description: { - zh: "功能类型枚举名" - en: "Function type enum name" - } - type: string - required: true - }, - { - name: "config_id" - description: { - zh: "模型配置ID" - en: "Model config id" - } - type: string - required: true - }, - { - name: "model_index" - description: { - zh: "可选,模型索引" - en: "Optional model index" - } - type: integer - required: false - } - ] - }, - { - name: "test_model_config_connection" - description: { - zh: '''按设置页同等逻辑测试某个模型配置。''' - en: '''Run settings-UI-equivalent tests for one model config.''' - } - parameters: [ - { - name: "config_id" - description: { - zh: "目标配置ID" - en: "Target config id" - } - type: string - required: true - }, - { - name: "model_index" - description: { - zh: "可选,模型索引" - en: "Optional model index" - } - type: integer - required: false - } - ] - }, - { - name: "ping_mcp" - description: { - zh: '''直通 use_package 的探测工具:用于快速测试某个 package 是否可被加载(MCP/Skill/Sandbox 三兼容)。''' - en: '''A pass-through probe for use_package: quickly test whether a package can be loaded (tri-compatible across MCP/Skill/Sandbox).''' - } - parameters: [ - { - name: "package_name" - description: { - zh: "要探测的包名" - en: "Package name to probe" - } - type: string - required: true - } - ] - } - ] -}*/ - -const operitEditorPackage = (function () { - -async function operit_editor(params: { query?: string }) { - try { - const { query } = params ?? {}; - complete({ - success: true, - message: "配置排查手册已加载(MCP/Skill/Sandbox Package/沙盒包调试烧录/功能模型与模型配置/TTS-STT语音服务),将按配置链路执行排查。", - data: { - query: query ?? "" - } - }); - } catch (error: unknown) { - complete({ - success: false, - message: get_error_message(error) - }); - } -} - -async function how_make_skill() { - try { - const locale = (getLang() ?? "").toLowerCase(); - const lang: "zh" | "en" | "both" = locale.startsWith("zh") ? "zh" : locale.startsWith("en") ? "en" : "both"; - - const zh = `如何制作 skill(简版) -1. 先创建目录:/sdcard/Download/Operit/skills// -2. 必备文件:SKILL.md -3. 在 SKILL.md 顶部用 Markdown 元数据(frontmatter)写 name、description,例如: ---- -name: your_skill_name -description: 用一句话说明这个 skill 做什么 ---- -4. 元数据后再写正文:适用场景、执行步骤、约束边界、期望输出 -5. 可选内容:scripts/、templates/、examples/、assets/;在 SKILL.md 里用相对路径引用 -6. 实践建议:优先下载现成 skill,直接解压过来,并确保目录下有 SKILL.md。`; - - const en = `How to make a skill (quick guide) -1. Create a directory: /sdcard/Download/Operit/skills// -2. Required file: SKILL.md -3. At the top of SKILL.md, use Markdown metadata (frontmatter) for name and description, for example: ---- -name: your_skill_name -description: one-line summary of what this skill does ---- -4. After metadata, write the main sections: use cases, workflow steps, constraints, expected outputs -5. Optional content: scripts/, templates/, examples/, assets/; reference them from SKILL.md using relative paths -6. Practical tip: download an existing skill, extract it directly, and ensure the directory contains SKILL.md`; - - const message = lang === "zh" ? zh : lang === "en" ? en : `${zh}\n\n---\n\n${en}`; - - complete({ - success: true, - message, - data: { - lang, - zh, - en - } - }); - } catch (error: unknown) { - complete({ - success: false, - message: get_error_message(error) - }); - } -} - -async function list_sandbox_packages() { - try { - const result = await Tools.SoftwareSettings.listSandboxPackages(); - complete({ - success: true, - message: `Sandbox package list fetched: ${String(result.totalCount)} package(s).`, - data: result - }); - } catch (error: unknown) { - complete({ - success: false, - message: get_error_message(error) - }); - } -} - -async function set_sandbox_package_enabled(params?: { package_name?: string; enabled?: boolean | string | number }) { - try { - const packageName = params?.package_name ?? ""; - const enabled = params?.enabled ?? false; - const result = await Tools.SoftwareSettings.setSandboxPackageEnabled(packageName, enabled); - complete({ - success: true, - message: result.message || "Sandbox package switch updated.", - data: result - }); - } catch (error: unknown) { - complete({ - success: false, - message: get_error_message(error) - }); - } -} - -const SANDBOX_EXTERNAL_PACKAGES_DIR = "/sdcard/Android/data/com.ai.assistance.operit/files/packages"; -const TOOLPKG_DEBUG_INSTALL_ACTION = "com.ai.assistance.operit.DEBUG_INSTALL_TOOLPKG"; -const TOOLPKG_DEBUG_INSTALL_COMPONENT = - "com.ai.assistance.operit/.core.tools.packTool.ToolPkgDebugInstallReceiver"; -const SANDBOX_SCRIPT_EXECUTION_ACTION = "com.ai.assistance.operit.EXECUTE_JS"; -const SANDBOX_SCRIPT_EXECUTION_COMPONENT = - "com.ai.assistance.operit/com.ai.assistance.operit.core.tools.javascript.ScriptExecutionReceiver"; -const SANDBOX_SCRIPT_EXECUTION_MODE_SCRIPT = "script"; -const SANDBOX_SCRIPT_EXECUTION_MODE_CODE = "code"; -const SANDBOX_JS_TEMP_DIR = "/sdcard/Android/data/com.ai.assistance.operit/js_temp"; -const DEFAULT_SANDBOX_REFRESH_TIMEOUT_MS = 1500; -const DEFAULT_TOOLPKG_INSTALL_WAIT_MS = 1500; -const DEFAULT_SANDBOX_SCRIPT_WAIT_MS = 15000; -const JS_METADATA_BLOCK_PATTERN = /\/\*\s*METADATA([\s\S]*?)\*\//m; -const JS_PACKAGE_NAME_PATTERN = /^\s*["']?name["']?\s*:\s*["']([^"']+)["']/m; -const TOOLPKG_ID_PATTERN = /^\s*["']?toolpkg_id["']?\s*:\s*["']([^"']+)["']/m; -const TOOLPKG_MAIN_PATTERN = /^\s*["']?main["']?\s*:\s*["']([^"']+)["']/m; -const TOOLPKG_SUBPACKAGE_ID_PATTERN = /^\s*["']?id["']?\s*:\s*["']([^"']+)["']/gm; -const TOOLPKG_SKIP_DIR_NAMES = new Set([".git", "__pycache__"]); -const TOOLPKG_SKIP_FILE_NAMES = new Set([".DS_Store", "Thumbs.db"]); - -type SandboxPackageEntry = SandboxPackageResultItem; -type SandboxPackagesPayload = SandboxPackagesResultData; - -type SandboxRefreshResult = { - payload: SandboxPackagesPayload | null; - packageEntry: SandboxPackageEntry | null; -}; - -function collect_related_package_load_errors( - payload: SandboxPackagesPayload | null, - packageName: string, - ...relatedPaths: Array -): Record { - const normalizedPackageName = normalize_package_key(packageName); - const normalizedPaths = relatedPaths.map((path) => String(path ?? "").trim()).filter(Boolean); - const packageLoadErrors = payload?.packageLoadErrors; - if (!packageLoadErrors) { - return {}; - } - - return Object.fromEntries( - Object.entries(packageLoadErrors).filter(([key, value]) => { - const normalizedKey = normalize_package_key(key); - const message = String(value ?? ""); - return ( - normalizedKey === normalizedPackageName || - message.toLowerCase().includes(normalizedPackageName) || - normalizedPaths.some((path) => message.includes(path)) - ); - }) - ); -} - -type JsPackageSourceInfo = { - packageName: string; - metadataBlock: string; -}; - -type ToolPkgManifestSummary = { - packageId: string; - mainEntry: string; - subpackageIds: string[]; -}; - -type ToolPkgResolvedSource = { - sourceKind: "folder" | "archive"; - sourcePath: string; - folderPath: string; - manifestPath: string; - packageId: string; - mainEntry: string; - subpackageIds: string[]; - archivePath?: string; - temporaryPaths: string[]; -}; - -function normalize_android_path(raw?: string): string { - const normalized = String(raw ?? "").trim().replace(/\\/g, "/"); - if (!normalized) return ""; - if (/^[a-zA-Z]+:\/\//.test(normalized)) return normalized; - if (normalized.startsWith("/")) return normalized; - if (normalized.startsWith("sdcard/")) return `/${normalized}`; - if (normalized.startsWith("Android/") || normalized.startsWith("Download/")) { - return `/sdcard/${normalized}`; - } - return normalized; -} - -function normalize_package_key(raw?: string): string { - return String(raw ?? "").trim().toLowerCase(); -} - -function normalize_directory_path(path: string): string { - const normalized = normalize_android_path(path).replace(/\/+/g, "/"); - if (normalized === "/") return normalized; - return normalized.replace(/\/+$/, ""); -} - -function same_android_path(left: string, right: string): boolean { - return normalize_directory_path(left) === normalize_directory_path(right); -} - -function path_dirname(path: string): string { - const normalized = normalize_directory_path(path); - const index = normalized.lastIndexOf("/"); - if (index < 0) return ""; - if (index === 0) return "/"; - return normalized.slice(0, index); -} - -function path_basename(path: string): string { - const normalized = normalize_directory_path(path); - const index = normalized.lastIndexOf("/"); - return index >= 0 ? normalized.slice(index + 1) : normalized; -} - -function path_join(...parts: string[]): string { - const filtered = parts - .map((part) => String(part ?? "").trim().replace(/\\/g, "/")) - .filter(Boolean); - if (filtered.length === 0) return ""; - - const leadingSlash = filtered[0].startsWith("/"); - const joined = filtered.map((part) => part.replace(/^\/+|\/+$/g, "")).filter(Boolean).join("/"); - return leadingSlash ? `/${joined}` : joined; -} - -function safe_debug_file_stem(raw: string, fallback: string): string { - const normalized = String(raw ?? "").trim().replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^[_\.]+|[_\.]+$/g, ""); - return normalized || fallback; -} - -function parse_boolean_like(value: unknown, defaultValue: boolean): boolean { - if (value === undefined || value === null || value === "") return defaultValue; - if (typeof value === "boolean") return value; - if (typeof value === "number") return value !== 0; - - const normalized = String(value).trim().toLowerCase(); - if (!normalized) return defaultValue; - if (["true", "1", "yes", "on"].includes(normalized)) return true; - if (["false", "0", "no", "off"].includes(normalized)) return false; - return defaultValue; -} - -function parse_integer_like(value: unknown, defaultValue: number): number { - if (value === undefined || value === null || value === "") return defaultValue; - const parsed = Number(value); - return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : defaultValue; -} - -async function android_path_exists(path: string): Promise { - const result = await Tools.Files.exists(path, "android"); - return result.exists; -} - -async function get_android_file_type(path: string): Promise { - const result = await Tools.Files.info(path, "android"); - return result.fileType.trim().toLowerCase(); -} - -async function ensure_android_directory(path: string) { - await Tools.Files.mkdir(path, true, "android"); -} - -async function delete_android_path_if_exists(path: string) { - if (!path) return; - if (!(await android_path_exists(path))) return; - await Tools.Files.deleteFile(path, true, "android"); -} - -async function cleanup_android_paths(paths: string[]) { - const uniquePaths = Array.from(new Set(paths.map((path) => normalize_android_path(path)).filter(Boolean))); - uniquePaths.sort((left, right) => right.length - left.length); - - for (const path of uniquePaths) { - try { - await delete_android_path_if_exists(path); - } catch { - // Ignore cleanup failures. - } - } -} - -async function read_android_text_file(path: string): Promise { - const result = await Tools.Files.read({ path, environment: "android" }); - return result.content; -} - -function parse_json_record(raw: string): Record | null { - if (!raw) return null; - try { - const parsed = JSON.parse(raw); - if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null; - return parsed as Record; - } catch { - return null; - } -} - -function find_sandbox_package_entry( - payload: SandboxPackagesPayload | null, - packageName: string -): SandboxPackageEntry | null { - const targetKey = normalize_package_key(packageName); - const packages = payload?.packages ?? []; - return ( - packages.find((entry) => normalize_package_key(entry?.packageName) === targetKey) ?? null - ); -} - -async function refresh_sandbox_packages_until( - packageName: string, - timeoutMs: number -): Promise { - const deadline = Date.now() + Math.max(0, timeoutMs); - let lastPayload: SandboxPackagesPayload | null = null; - let lastEntry: SandboxPackageEntry | null = null; - - while (true) { - lastPayload = await Tools.SoftwareSettings.listSandboxPackages(); - lastEntry = find_sandbox_package_entry(lastPayload, packageName); - if (lastEntry) { - return { - payload: lastPayload, - packageEntry: lastEntry - }; - } - - if (Date.now() >= deadline) { - return { - payload: lastPayload, - packageEntry: lastEntry - }; - } - - await Tools.System.sleep(Math.min(300, Math.max(50, deadline - Date.now()))); - } -} - -async function wait_for_android_file(path: string, timeoutMs: number): Promise { - const deadline = Date.now() + Math.max(0, timeoutMs); - - while (true) { - if (await android_path_exists(path)) { - return true; - } - if (Date.now() >= deadline) { - return false; - } - await Tools.System.sleep(Math.min(300, Math.max(50, deadline - Date.now()))); - } -} - -function parse_json_text(raw: string): unknown | null { - const text = String(raw ?? "").trim(); - if (!text) return null; - try { - return JSON.parse(text); - } catch { - return null; - } -} - -function extract_js_metadata_block(sourceText: string, sourcePath: string): string { - const match = JS_METADATA_BLOCK_PATTERN.exec(sourceText); - if (!match) { - throw new Error(`Missing METADATA block: ${sourcePath}`); - } - return match[1].trim(); -} - -function parse_js_package_source(sourceText: string, sourcePath: string): JsPackageSourceInfo { - const metadataBlock = extract_js_metadata_block(sourceText, sourcePath); - const packageName = JS_PACKAGE_NAME_PATTERN.exec(metadataBlock)?.[1]?.trim() ?? ""; - if (!packageName) { - throw new Error(`Missing package metadata name: ${sourcePath}`); - } - return { - packageName, - metadataBlock - }; -} - -async function delete_duplicate_external_js_package_files(packageName: string, keepPath: string) { - const removedPaths: string[] = []; - const listing = await Tools.Files.list(SANDBOX_EXTERNAL_PACKAGES_DIR, "android"); - - for (const entry of listing?.entries ?? []) { - const entryName = String(entry?.name ?? "").trim(); - if (!entryName || entry?.isDirectory || !entryName.toLowerCase().endsWith(".js")) { - continue; - } - - const candidatePath = path_join(SANDBOX_EXTERNAL_PACKAGES_DIR, entryName); - if (same_android_path(candidatePath, keepPath)) { - continue; - } - - try { - const candidateText = await read_android_text_file(candidatePath); - const candidateInfo = parse_js_package_source(candidateText, candidatePath); - if (normalize_package_key(candidateInfo.packageName) !== normalize_package_key(packageName)) { - continue; - } - - await Tools.Files.deleteFile(candidatePath, false, "android"); - removedPaths.push(candidatePath); - } catch { - // Ignore files that cannot be parsed as sandbox packages. - } - } - - return removedPaths; -} - -function parse_toolpkg_manifest_text(text: string, manifestPath: string): ToolPkgManifestSummary { - let packageId = ""; - let mainEntry = ""; - let subpackageIds: string[] = []; - - try { - const parsed = JSON.parse(text) as { - toolpkg_id?: unknown; - main?: unknown; - subpackages?: Array<{ id?: unknown }>; - }; - if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { - packageId = String(parsed.toolpkg_id ?? "").trim(); - mainEntry = String(parsed.main ?? "").trim(); - if (Array.isArray(parsed.subpackages)) { - subpackageIds = parsed.subpackages - .map((subpackage) => String(subpackage?.id ?? "").trim()) - .filter(Boolean); - } - } - } catch { - // HJSON-like manifests will fall back to regex parsing below. - } - - if (!packageId) { - packageId = TOOLPKG_ID_PATTERN.exec(text)?.[1]?.trim() ?? ""; - } - if (!mainEntry) { - mainEntry = TOOLPKG_MAIN_PATTERN.exec(text)?.[1]?.trim() ?? ""; - } - if (subpackageIds.length === 0) { - const matches: string[] = []; - const pattern = new RegExp(TOOLPKG_SUBPACKAGE_ID_PATTERN.source, TOOLPKG_SUBPACKAGE_ID_PATTERN.flags); - let match: RegExpExecArray | null; - while ((match = pattern.exec(text)) !== null) { - const subpackageId = match[1]?.trim() ?? ""; - if (subpackageId) { - matches.push(subpackageId); - } - } - subpackageIds = matches; - } - - if (!packageId) { - throw new Error(`manifest.toolpkg_id is required: ${manifestPath}`); - } - if (!mainEntry) { - throw new Error(`manifest.main is required: ${manifestPath}`); - } - - return { - packageId, - mainEntry: mainEntry.replace(/\\/g, "/").replace(/^\/+/, ""), - subpackageIds: Array.from(new Set(subpackageIds)) - }; -} - -async function find_toolpkg_manifest_in_folder(folderPath: string): Promise { - const manifestJson = path_join(folderPath, "manifest.json"); - if (await android_path_exists(manifestJson)) { - return manifestJson; - } - - const manifestHjson = path_join(folderPath, "manifest.hjson"); - if (await android_path_exists(manifestHjson)) { - return manifestHjson; - } - - throw new Error(`Missing manifest.json or manifest.hjson in folder: ${folderPath}`); -} - -async function resolve_toolpkg_source(rawSourcePath: string): Promise { - const sourcePath = normalize_android_path(rawSourcePath); - if (!sourcePath) { - throw new Error("Missing required parameter: source_path"); - } - if (!(await android_path_exists(sourcePath))) { - throw new Error(`Source path does not exist: ${sourcePath}`); - } - - const sourceType = await get_android_file_type(sourcePath); - let sourceKind: "folder" | "archive" = "folder"; - let folderPath = sourcePath; - let archivePath: string | undefined; - const temporaryPaths: string[] = []; - - const lowerBaseName = path_basename(sourcePath).toLowerCase(); - if (sourceType === "directory") { - folderPath = sourcePath; - } else if (sourceType === "file" && (lowerBaseName === "manifest.json" || lowerBaseName === "manifest.hjson")) { - folderPath = path_dirname(sourcePath); - } else if (sourceType === "file" && lowerBaseName.endsWith(".toolpkg")) { - sourceKind = "archive"; - archivePath = sourcePath; - const tempExtractDir = path_join( - OPERIT_CLEAN_ON_EXIT_DIR, - `operit_editor_toolpkg_extract_${safe_debug_file_stem(lowerBaseName.replace(/\.toolpkg$/i, ""), "toolpkg")}_${Date.now()}` - ); - await ensure_android_directory(tempExtractDir); - await Tools.Files.unzip(sourcePath, tempExtractDir, "android"); - folderPath = tempExtractDir; - temporaryPaths.push(tempExtractDir); - } else { - throw new Error( - "ToolPkg source must be a folder, manifest.json/manifest.hjson, or an existing .toolpkg file" - ); - } - - const manifestPath = await find_toolpkg_manifest_in_folder(folderPath); - const manifestText = await read_android_text_file(manifestPath); - const manifest = parse_toolpkg_manifest_text(manifestText, manifestPath); - const mainPath = path_join(folderPath, manifest.mainEntry); - if (!(await android_path_exists(mainPath))) { - throw new Error(`manifest.main file does not exist: ${manifestPath} -> ${manifest.mainEntry}`); - } - - return { - sourceKind, - sourcePath, - folderPath, - manifestPath, - packageId: manifest.packageId, - mainEntry: manifest.mainEntry, - subpackageIds: manifest.subpackageIds, - archivePath, - temporaryPaths - }; -} - -async function build_toolpkg_archive_from_folder(source: ToolPkgResolvedSource) { - const tempBuildDir = path_join( - OPERIT_CLEAN_ON_EXIT_DIR, - `operit_editor_toolpkg_build_${safe_debug_file_stem(source.packageId, "toolpkg")}_${Date.now()}` - ); - await ensure_android_directory(tempBuildDir); - const archivePath = path_join(tempBuildDir, `${safe_debug_file_stem(source.packageId, "toolpkg")}.toolpkg`); - await Tools.Files.zip(source.folderPath, archivePath, "android", false); - - if (!(await android_path_exists(archivePath))) { - throw new Error(`Failed to create ToolPkg archive: ${archivePath}`); - } - - return { - archivePath, - temporaryPaths: [tempBuildDir] - }; -} - -function parse_requested_package_ids(raw?: string): string[] { - const input = String(raw ?? "").trim(); - if (!input) return []; - return Array.from(new Set(input.split(/[\r\n,]+/).map((item) => item.trim()).filter(Boolean))); -} - -async function debug_install_js_package(params?: { - source_path?: string; - enable_after_install?: boolean | string | number; - activate_after_install?: boolean | string | number; -}) { - const logs: string[] = []; - const logStep = (message: string) => { - logs.push(message); - }; - const finish = (payload: { success: boolean; message: string; data?: Record }) => - complete({ - ...payload, - data: { - ...(payload.data ?? {}), - logs - } - }); - - try { - const sourcePath = normalize_android_path(params?.source_path); - logStep(`Resolved source_path -> ${sourcePath || ""}`); - if (!sourcePath) { - finish({ - success: false, - message: "Missing required parameter: source_path" - }); - return; - } - - const sourceType = await get_android_file_type(sourcePath); - logStep(`Source type detected -> ${sourceType}`); - if (sourceType !== "file") { - finish({ - success: false, - message: `JS source must be a file: ${sourcePath}` - }); - return; - } - if (!sourcePath.toLowerCase().endsWith(".js")) { - finish({ - success: false, - message: `JS debug install only supports .js files: ${sourcePath}` - }); - return; - } - - const sourceText = await read_android_text_file(sourcePath); - const packageInfo = parse_js_package_source(sourceText, sourcePath); - logStep(`Parsed package info -> packageName=${packageInfo.packageName}`); - const enableAfterInstall = parse_boolean_like(params?.enable_after_install, true); - const activateAfterInstall = parse_boolean_like(params?.activate_after_install, true); - const shouldEnable = enableAfterInstall || activateAfterInstall; - logStep( - `Install options -> enableAfterInstall=${String(enableAfterInstall)}, activateAfterInstall=${String( - activateAfterInstall - )}, shouldEnable=${String(shouldEnable)}` - ); - - await ensure_android_directory(SANDBOX_EXTERNAL_PACKAGES_DIR); - const targetPath = path_join( - SANDBOX_EXTERNAL_PACKAGES_DIR, - `${safe_debug_file_stem(packageInfo.packageName, "debug_js_package")}.js` - ); - logStep(`Target install path -> ${targetPath}`); - const copied = !same_android_path(sourcePath, targetPath); - if (copied) { - logStep("Source and target differ; replacing target file before copy."); - await delete_android_path_if_exists(targetPath); - await Tools.Files.copy(sourcePath, targetPath, false, "android", "android"); - logStep("Package file copied to external sandbox directory."); - } else { - logStep("Source path already matches target path; skipping file copy."); - } - if (!(await android_path_exists(targetPath))) { - finish({ - success: false, - message: `Installed JS package file is missing after copy: ${targetPath}` - }); - return; - } - logStep("Verified installed JS package file exists."); - - const removedDuplicateFiles = await delete_duplicate_external_js_package_files(packageInfo.packageName, targetPath); - logStep(`Duplicate cleanup completed -> removed ${removedDuplicateFiles.length} file(s).`); - const refresh = await refresh_sandbox_packages_until( - packageInfo.packageName, - DEFAULT_SANDBOX_REFRESH_TIMEOUT_MS - ); - logStep( - `Sandbox refresh completed -> found=${String(Boolean(refresh.packageEntry))}, builtIn=${String( - refresh.packageEntry?.isBuiltIn ?? false - )}` - ); - if (!refresh.packageEntry) { - finish({ - success: false, - message: `Sandbox package did not appear after refresh: ${packageInfo.packageName}`, - data: { - package_name: packageInfo.packageName, - source_path: sourcePath, - target_path: targetPath, - refresh_result: refresh.payload - } - }); - return; - } - if (refresh.packageEntry.isBuiltIn) { - finish({ - success: false, - message: `External JS package '${packageInfo.packageName}' did not take precedence over a built-in package with the same name.`, - data: { - package: refresh.packageEntry, - source_path: sourcePath, - target_path: targetPath - } - }); - return; - } - - let enableResult: SandboxPackageUpdateResultData | null = null; - if (shouldEnable) { - logStep(`Enabling sandbox package -> ${packageInfo.packageName}`); - enableResult = await Tools.SoftwareSettings.setSandboxPackageEnabled(packageInfo.packageName, true); - logStep(`Enable result -> ${enableResult.message || ""}`); - } else { - logStep("Enable step skipped by configuration."); - } - - let activateResult: string | null = null; - if (activateAfterInstall) { - logStep(`Activating package via use_package -> ${packageInfo.packageName}`); - activateResult = await Tools.System.usePackage(packageInfo.packageName); - logStep(`Activation result -> ${activateResult || ""}`); - } else { - logStep("Activation step skipped by configuration."); - } - - finish({ - success: true, - message: `Debug JS package installed: ${packageInfo.packageName}`, - data: { - package_name: packageInfo.packageName, - source_path: sourcePath, - target_path: targetPath, - copied, - removed_duplicate_files: removedDuplicateFiles, - package: refresh.packageEntry, - enable_after_install: shouldEnable, - activate_after_install: activateAfterInstall, - enable_result: enableResult, - activate_result: activateResult, - refresh_result: refresh.payload - } - }); - } catch (error: unknown) { - logStep(`Execution failed -> ${get_error_message(error)}`); - finish({ - success: false, - message: get_error_message(error) - }); - } -} - -async function debug_install_toolpkg(params?: { - source_path?: string; - reset_subpackage_states?: boolean | string | number; - activate_subpackages?: string; - wait_ms?: number | string; -}) { - const cleanupPaths: string[] = []; - const logs: string[] = []; - const logStep = (message: string) => { - logs.push(message); - }; - const finish = (payload: { success: boolean; message: string; data?: Record }) => - complete({ - ...payload, - data: { - ...(payload.data ?? {}), - logs - } - }); - let finalPayload: { success: boolean; message: string; data?: Record } | null = null; - - try { - const resolvedSource = await resolve_toolpkg_source(params?.source_path ?? ""); - logStep( - `Resolved ToolPkg source -> kind=${resolvedSource.sourceKind}, packageId=${resolvedSource.packageId}, sourcePath=${resolvedSource.sourcePath}` - ); - cleanupPaths.push(...resolvedSource.temporaryPaths); - if (resolvedSource.temporaryPaths.length > 0) { - logStep(`Registered temporary paths -> ${resolvedSource.temporaryPaths.join(", ")}`); - } - - let archivePath = resolvedSource.archivePath ?? ""; - if (resolvedSource.sourceKind === "folder") { - logStep("Source is a folder; building temporary .toolpkg archive."); - const builtArchive = await build_toolpkg_archive_from_folder(resolvedSource); - archivePath = builtArchive.archivePath; - cleanupPaths.push(...builtArchive.temporaryPaths); - logStep(`Built archive -> ${archivePath}`); - } - - await ensure_android_directory(SANDBOX_EXTERNAL_PACKAGES_DIR); - const targetPath = path_join( - SANDBOX_EXTERNAL_PACKAGES_DIR, - `${safe_debug_file_stem(resolvedSource.packageId, "toolpkg")}.toolpkg` - ); - logStep(`Target install path -> ${targetPath}`); - if (!same_android_path(archivePath, targetPath)) { - logStep("Archive path differs from target; replacing target archive before copy."); - await delete_android_path_if_exists(targetPath); - await Tools.Files.copy(archivePath, targetPath, false, "android", "android"); - logStep("ToolPkg archive copied to external sandbox directory."); - } else { - logStep("Archive path already matches target path; skipping archive copy."); - } - if (!(await android_path_exists(targetPath))) { - finalPayload = { - success: false, - message: `Installed ToolPkg archive is missing after copy: ${targetPath}` - }; - return; - } - logStep("Verified installed ToolPkg archive exists."); - - const resetSubpackageStates = parse_boolean_like(params?.reset_subpackage_states, true); - const waitMs = parse_integer_like(params?.wait_ms, DEFAULT_TOOLPKG_INSTALL_WAIT_MS); - logStep( - `Install options -> resetSubpackageStates=${String(resetSubpackageStates)}, waitMs=${String(waitMs)}` - ); - const broadcastResult = await Tools.System.sendBroadcast({ - action: TOOLPKG_DEBUG_INSTALL_ACTION, - component: TOOLPKG_DEBUG_INSTALL_COMPONENT, - extras: { - package_name: resolvedSource.packageId, - file_path: targetPath, - reset_subpackage_states: resetSubpackageStates - } - }); - logStep(`Debug install broadcast dispatched -> ${broadcastResult.result || ""}`); - - const refresh = await refresh_sandbox_packages_until(resolvedSource.packageId, waitMs); - const relatedLoadErrors = collect_related_package_load_errors( - refresh.payload, - resolvedSource.packageId, - resolvedSource.sourcePath, - archivePath, - targetPath - ); - logStep( - `Sandbox refresh completed -> found=${String(Boolean(refresh.packageEntry))}, builtIn=${String( - refresh.packageEntry?.isBuiltIn ?? false - )}` - ); - if (Object.keys(relatedLoadErrors).length > 0) { - logStep(`Related load errors -> ${JSON.stringify(relatedLoadErrors)}`); - } - if (!refresh.packageEntry) { - finalPayload = { - success: false, - message: `ToolPkg container did not appear after debug install: ${resolvedSource.packageId}`, - data: { - package_name: resolvedSource.packageId, - source_path: resolvedSource.sourcePath, - archive_path: targetPath, - broadcast_result: broadcastResult, - refresh_result: refresh.payload, - related_load_errors: relatedLoadErrors - } - }; - return; - } - if (refresh.packageEntry.isBuiltIn) { - finalPayload = { - success: false, - message: `Debug ToolPkg '${resolvedSource.packageId}' is shadowed by a built-in package with the same name.`, - data: { - package: refresh.packageEntry, - broadcast_result: broadcastResult, - refresh_result: refresh.payload, - related_load_errors: relatedLoadErrors - } - }; - return; - } - - const requestedSubpackages = parse_requested_package_ids(params?.activate_subpackages); - const knownSubpackageKeys = new Set(resolvedSource.subpackageIds.map(normalize_package_key)); - const unknownRequestedSubpackages = - knownSubpackageKeys.size === 0 - ? [] - : requestedSubpackages.filter((subpackageId) => !knownSubpackageKeys.has(normalize_package_key(subpackageId))); - const activationTargets = requestedSubpackages.filter( - (subpackageId) => !unknownRequestedSubpackages.includes(subpackageId) - ); - logStep( - `Subpackage activation plan -> requested=${requestedSubpackages.join(", ") || ""}, targets=${activationTargets.join(", ") || ""}, unknown=${unknownRequestedSubpackages.join(", ") || ""}` - ); - - const subpackageResults: Array<{ - subpackage_id: string; - enable_result: SandboxPackageUpdateResultData; - activate_result: string; - }> = []; - - for (const subpackageId of activationTargets) { - logStep(`Enabling subpackage -> ${subpackageId}`); - const enableResult = await Tools.SoftwareSettings.setSandboxPackageEnabled(subpackageId, true); - logStep(`Subpackage enable result [${subpackageId}] -> ${enableResult.message || ""}`); - const activateResult = await Tools.System.usePackage(subpackageId); - logStep(`Subpackage activate result [${subpackageId}] -> ${activateResult || ""}`); - subpackageResults.push({ - subpackage_id: subpackageId, - enable_result: enableResult, - activate_result: activateResult - }); - } - - finalPayload = { - success: true, - message: `Debug ToolPkg installed: ${resolvedSource.packageId}`, - data: { - package_name: resolvedSource.packageId, - source_kind: resolvedSource.sourceKind, - source_path: resolvedSource.sourcePath, - manifest_path: resolvedSource.manifestPath, - main_entry: resolvedSource.mainEntry, - archive_path: targetPath, - subpackage_ids: resolvedSource.subpackageIds, - reset_subpackage_states: resetSubpackageStates, - requested_activate_subpackages: requestedSubpackages, - unknown_requested_subpackages: unknownRequestedSubpackages, - subpackage_results: subpackageResults, - package: refresh.packageEntry, - broadcast_result: broadcastResult, - refresh_result: refresh.payload, - related_load_errors: relatedLoadErrors - } - }; - } catch (error: unknown) { - logStep(`Execution failed -> ${get_error_message(error)}`); - finalPayload = { - success: false, - message: get_error_message(error) - }; - } finally { - if (cleanupPaths.length > 0) { - logStep(`Cleaning temporary paths -> ${cleanupPaths.join(", ")}`); - } - await cleanup_android_paths(cleanupPaths); - if (cleanupPaths.length > 0) { - logStep("Temporary path cleanup completed."); - } - if (finalPayload) { - finish(finalPayload); - } - } -} - -async function debug_run_sandbox_script(params?: { - source_path?: string; - source_code?: string; - params_json?: string; - env_file_path?: string; - script_label?: string; - wait_ms?: number | string; -}) { - const logs: string[] = []; - const logStep = (message: string) => { - logs.push(message); - }; - const finish = (payload: { success: boolean; message: string; data?: Record }) => - complete({ - ...payload, - data: { - ...(payload.data ?? {}), - logs - } - }); - let finalPayload: { success: boolean; message: string; data?: Record } | null = null; - - try { - const sourcePath = normalize_android_path(params?.source_path); - const sourceCode = typeof params?.source_code === "string" ? params.source_code : ""; - const hasInlineCode = sourceCode.trim().length > 0; - const waitMs = parse_integer_like(params?.wait_ms, DEFAULT_SANDBOX_SCRIPT_WAIT_MS); - const paramsJson = String(params?.params_json ?? "{}").trim() || "{}"; - const parsedParams = parse_json_text(paramsJson); - const envFilePath = normalize_android_path(params?.env_file_path); - const scriptLabel = safe_debug_file_stem(String(params?.script_label ?? "").trim(), "sandbox_script"); - - logStep( - `Resolved input -> sourcePath=${sourcePath || ""}, hasInlineCode=${String(hasInlineCode)}, waitMs=${String(waitMs)}` - ); - - if (!sourcePath && !hasInlineCode) { - finalPayload = { - success: false, - message: "Either source_path or source_code is required." - }; - return; - } - - if (sourcePath) { - const sourceType = await get_android_file_type(sourcePath); - logStep(`Source type detected -> ${sourceType}`); - if (sourceType !== "file") { - finalPayload = { - success: false, - message: `Sandbox script source must be a file: ${sourcePath}` - }; - return; - } - } - - if (!parsedParams || typeof parsedParams !== "object" || Array.isArray(parsedParams)) { - finalPayload = { - success: false, - message: `params_json must be a JSON object: ${paramsJson}` - }; - return; - } - logStep("params_json parsed successfully."); - - if (envFilePath) { - const envType = await get_android_file_type(envFilePath); - logStep(`Env file type detected -> ${envType}`); - if (envType !== "file") { - finalPayload = { - success: false, - message: `env_file_path must be a file: ${envFilePath}` - }; - return; - } - } - - const executionMode = hasInlineCode ? SANDBOX_SCRIPT_EXECUTION_MODE_CODE : SANDBOX_SCRIPT_EXECUTION_MODE_SCRIPT; - const scriptIdentityPath = - sourcePath || path_join(SANDBOX_JS_TEMP_DIR, `${scriptLabel}_${Date.now()}.inline.js`); - logStep(`Execution mode -> ${executionMode}`); - logStep(`Execution target -> ${scriptIdentityPath}`); - - const executionResult = await Tools.SoftwareSettings.executeSandboxScriptDirect({ - source_path: sourcePath || undefined, - source_code: hasInlineCode ? sourceCode : undefined, - params_json: paramsJson, - env_file_path: envFilePath || undefined, - script_label: scriptLabel, - wait_ms: waitMs - }); - logStep( - `Direct execution tool completed -> success=${String(Boolean(executionResult.success))}, durationMs=${String( - executionResult.durationMs ?? "" - )}` - ); - - finalPayload = { - success: executionResult.success, - message: executionResult.success - ? "Sandbox script executed successfully." - : String(executionResult.error ?? "Sandbox script execution failed."), - data: { - execution_mode: executionMode, - source_path: sourcePath, - has_inline_code: hasInlineCode, - env_file_path: envFilePath || null, - params_json: paramsJson, - execution_result: executionResult - } - }; - } catch (error: unknown) { - logStep(`Execution failed -> ${get_error_message(error)}`); - finalPayload = { - success: false, - message: get_error_message(error) - }; - } finally { - finish( - finalPayload ?? { - success: false, - message: "Sandbox script execution did not produce a final result." - } - ); - } -} - -async function read_environment_variable(params?: { key?: string }) { - try { - const key = (params?.key ?? "").trim(); - if (!key) { - complete({ - success: false, - message: "Missing required parameter: key" - }); - return; - } - - const result = await Tools.SoftwareSettings.readEnvironmentVariable(key); - - complete({ - success: true, - message: result.exists ? `Environment variable read: ${key}` : `Environment variable not set: ${key}`, - data: result - }); - } catch (error: unknown) { - complete({ - success: false, - message: get_error_message(error) - }); - } -} - -async function write_environment_variable(params?: { key?: string; value?: string }) { - try { - const key = (params?.key ?? "").trim(); - if (!key) { - complete({ - success: false, - message: "Missing required parameter: key" - }); - return; - } - - const value = params?.value ?? ""; - const result = await Tools.SoftwareSettings.writeEnvironmentVariable(key, String(value)); - - complete({ - success: true, - message: result.cleared ? `Environment variable cleared: ${key}` : `Environment variable written: ${key}`, - data: result - }); - } catch (error: unknown) { - complete({ - success: false, - message: get_error_message(error) - }); - } -} - -async function restart_mcp_with_logs(params?: { timeout_ms?: number | string }) { - try { - const timeoutMs = params?.timeout_ms; - const result = await Tools.SoftwareSettings.restartMcpWithLogs(timeoutMs); - complete({ - success: true, - message: result.timedOut - ? `MCP restart timed out after ${String(result.elapsedMs)}ms.` - : `MCP restart completed: ${String(result.successCount)} success, ${String(result.failedCount)} failed.`, - data: result - }); - } catch (error: unknown) { - complete({ - success: false, - message: get_error_message(error) - }); - } -} - -async function get_speech_services_config() { - try { - const result = await Tools.SoftwareSettings.getSpeechServicesConfig(); - const parsed = result; - - complete({ - success: true, - message: "Speech services config fetched.", - data: { - parsed - } - }); - } catch (error: unknown) { - complete({ - success: false, - message: get_error_message(error) - }); - } -} - -type TtsServiceType = - | "SIMPLE_TTS" - | "HTTP_TTS" - | "OPENAI_WS_TTS" - | "SILICONFLOW_TTS" - | "MINIMAX_TTS" - | "MIMO_TTS" - | "DOUBAO_TTS" - | "OPENAI_TTS" - | "VITS_TTS"; - -type SttServiceType = - | "SHERPA_NCNN" - | "OPENAI_STT" - | "DEEPGRAM_STT"; - -type SpeechServicesConfigUpdateParams = { - tts_service_type?: TtsServiceType; - tts_url_template?: string; - tts_api_key?: string; - tts_headers?: string | Record; - tts_http_method?: string; - tts_request_body?: string; - tts_content_type?: string; - tts_locale?: string; - tts_voice_id?: string; - tts_model_name?: string; - tts_vits_package_path?: string; - tts_vits_speaker_id?: string; - tts_vits_options?: string | Record; - tts_response_pipeline?: string | Array<{ - type: string; - path?: string; - headers?: Record; - }>; - tts_cleaner_regexs?: string | string[]; - tts_speech_rate?: number; - tts_pitch?: number; - stt_service_type?: SttServiceType; - stt_endpoint_url?: string; - stt_api_key?: string; - stt_model_name?: string; -}; - -async function set_speech_services_config(params?: SpeechServicesConfigUpdateParams) { - try { - const updates = { ...(params ?? {}) }; - const result = await Tools.SoftwareSettings.setSpeechServicesConfig(updates); - const parsed = result; - - complete({ - success: true, - message: "Speech services config updated.", - data: { - updates, - parsed - } - }); - } catch (error: unknown) { - complete({ - success: false, - message: get_error_message(error) - }); - } -} - -async function test_tts_playback(params?: { - text?: string; - interrupt?: boolean; - speech_rate?: number; - pitch?: number; -}) { - try { - const text = (params?.text ?? "").trim(); - if (!text) { - complete({ - success: false, - message: "Missing required parameter: text" - }); - return; - } - const options = { ...(params ?? {}) }; - delete options.text; - const result = await Tools.SoftwareSettings.testTtsPlayback(text, options); - const success = result.playbackTriggered; - const detailMessage = result.errorMessage?.trim() || "TTS playback test failed."; - complete({ - success, - message: success ? "TTS playback test triggered." : detailMessage, - data: result - }); - } catch (error: unknown) { - complete({ - success: false, - message: get_error_message(error) - }); - } -} - -async function list_model_configs() { - try { - const result = await Tools.SoftwareSettings.listModelConfigs(); - complete({ - success: true, - message: "Model configs listed.", - data: result - }); - } catch (error: unknown) { - complete({ - success: false, - message: get_error_message(error) - }); - } -} - -type ModelConfigProviderType = - | "OPENAI" - | "OPENAI_RESPONSES" - | "OPENAI_RESPONSES_GENERIC" - | "OPENAI_GENERIC" - | "ANTHROPIC" - | "ANTHROPIC_GENERIC" - | "GOOGLE" - | "GEMINI_GENERIC" - | "BAIDU" - | "ALIYUN" - | "XUNFEI" - | "ZHIPU" - | "BAICHUAN" - | "MOONSHOT" - | "DEEPSEEK" - | "MISTRAL" - | "SILICONFLOW" - | "IFLOW" - | "OPENROUTER" - | "FOUR_ROUTER" - | "NOUS_PORTAL" - | "INFINIAI" - | "ALIPAY_BAILING" - | "DOUBAO" - | "NVIDIA" - | "LMSTUDIO" - | "OLLAMA" - | "OPENAI_LOCAL" - | "MNN" - | "LLAMA_CPP" - | "PPINFRA" - | "NOVITA" - | "OTHER" - | (string & {}); - -async function create_model_config(params?: { - name?: string; - api_provider_type?: ModelConfigProviderType; - api_endpoint?: string; - api_key?: string; - model_name?: string; - max_tokens_enabled?: boolean; - max_tokens?: number; - temperature_enabled?: boolean; - temperature?: number; - top_p_enabled?: boolean; - top_p?: number; - top_k_enabled?: boolean; - top_k?: number; - presence_penalty_enabled?: boolean; - presence_penalty?: number; - frequency_penalty_enabled?: boolean; - frequency_penalty?: number; - repetition_penalty_enabled?: boolean; - repetition_penalty?: number; - custom_parameters?: string | Record; - custom_headers?: string | Record; - context_length?: number; - max_context_length?: number; - enable_max_context_mode?: boolean; - summary_token_threshold?: number; - enable_summary?: boolean; - enable_summary_by_message_count?: boolean; - summary_message_count_threshold?: number; - enable_direct_image_processing?: boolean; - enable_direct_audio_processing?: boolean; - enable_direct_video_processing?: boolean; - enable_google_search?: boolean; - enable_claude_1h_prompt_cache?: boolean; - enable_tool_call?: boolean; - mnn_forward_type?: number; - mnn_thread_count?: number; - llama_thread_count?: number; - llama_context_size?: number; - llama_gpu_layers?: number; - request_limit_per_minute?: number; - max_concurrent_requests?: number; -}) { - try { - const options = { ...(params ?? {}) }; - const result = await Tools.SoftwareSettings.createModelConfig(options); - complete({ - success: true, - message: "Model config created.", - data: result - }); - } catch (error: unknown) { - complete({ - success: false, - message: get_error_message(error) - }); - } -} - -async function update_model_config(params?: { - config_id?: string; - name?: string; - api_provider_type?: ModelConfigProviderType; - api_endpoint?: string; - api_key?: string; - model_name?: string; - max_tokens_enabled?: boolean; - max_tokens?: number; - temperature_enabled?: boolean; - temperature?: number; - top_p_enabled?: boolean; - top_p?: number; - top_k_enabled?: boolean; - top_k?: number; - presence_penalty_enabled?: boolean; - presence_penalty?: number; - frequency_penalty_enabled?: boolean; - frequency_penalty?: number; - repetition_penalty_enabled?: boolean; - repetition_penalty?: number; - custom_parameters?: string | Record; - custom_headers?: string | Record; - context_length?: number; - max_context_length?: number; - enable_max_context_mode?: boolean; - summary_token_threshold?: number; - enable_summary?: boolean; - enable_summary_by_message_count?: boolean; - summary_message_count_threshold?: number; - enable_direct_image_processing?: boolean; - enable_direct_audio_processing?: boolean; - enable_direct_video_processing?: boolean; - enable_google_search?: boolean; - enable_claude_1h_prompt_cache?: boolean; - enable_tool_call?: boolean; - mnn_forward_type?: number; - mnn_thread_count?: number; - llama_thread_count?: number; - llama_context_size?: number; - llama_gpu_layers?: number; - request_limit_per_minute?: number; - max_concurrent_requests?: number; -}) { - try { - const configId = (params?.config_id ?? "").trim(); - if (!configId) { - complete({ - success: false, - message: "Missing required parameter: config_id" - }); - return; - } - - const { config_id: _ignoredConfigId, ...updates } = params ?? {}; - const result = await Tools.SoftwareSettings.updateModelConfig(configId, updates); - complete({ - success: true, - message: "Model config updated.", - data: result - }); - } catch (error: unknown) { - complete({ - success: false, - message: get_error_message(error) - }); - } -} - -async function delete_model_config(params?: { config_id?: string }) { - try { - const configId = (params?.config_id ?? "").trim(); - if (!configId) { - complete({ - success: false, - message: "Missing required parameter: config_id" - }); - return; - } - const result = await Tools.SoftwareSettings.deleteModelConfig(configId); - complete({ - success: true, - message: "Model config deleted.", - data: result - }); - } catch (error: unknown) { - complete({ - success: false, - message: get_error_message(error) - }); - } -} - -async function list_function_model_configs() { - try { - const result = await Tools.SoftwareSettings.listFunctionModelConfigs(); - complete({ - success: true, - message: "Function model bindings listed.", - data: result - }); - } catch (error: unknown) { - complete({ - success: false, - message: get_error_message(error) - }); - } -} - -async function get_function_model_config(params?: { function_type?: string }) { - try { - const functionType = (params?.function_type ?? "").trim(); - if (!functionType) { - complete({ - success: false, - message: "Missing required parameter: function_type" - }); - return; - } - - const result = await Tools.SoftwareSettings.getFunctionModelConfig(functionType); - const config = result?.config ?? {}; - const { - contextLength: _contextLength, - maxContextLength: _maxContextLength, - enableMaxContextMode: _enableMaxContextMode, - summaryTokenThreshold: _summaryTokenThreshold, - enableSummary: _enableSummary, - enableSummaryByMessageCount: _enableSummaryByMessageCount, - summaryMessageCountThreshold: _summaryMessageCountThreshold, - ...configWithoutContextSummary - } = config; - - const filteredResult = { - ...result, - config: configWithoutContextSummary - }; - - complete({ - success: true, - message: "Function model config fetched.", - data: filteredResult - }); - } catch (error: unknown) { - complete({ - success: false, - message: get_error_message(error) - }); - } -} - -type ContextSummaryConfigSnapshot = { - id?: string; - name?: string; - contextLength?: number; - maxContextLength?: number; - enableMaxContextMode?: boolean; - summaryTokenThreshold?: number; - enableSummary?: boolean; - enableSummaryByMessageCount?: boolean; - summaryMessageCountThreshold?: number; -}; - -type FunctionModelConfigDetailLike = { - configId?: string; - configName?: string; - modelIndex?: number; - actualModelIndex?: number; - selectedModel?: string; - config?: ContextSummaryConfigSnapshot; -}; - -type ModelConfigUpdateResultLike = { - config?: ContextSummaryConfigSnapshot; -}; - -function get_error_message(error: unknown): string { - return error instanceof Error ? error.message : "Unknown error"; -} - -function pick_context_summary_fields(config?: ContextSummaryConfigSnapshot) { - return { - context_length: config?.contextLength, - max_context_length: config?.maxContextLength, - enable_max_context_mode: config?.enableMaxContextMode, - summary_token_threshold: config?.summaryTokenThreshold, - enable_summary: config?.enableSummary, - enable_summary_by_message_count: config?.enableSummaryByMessageCount, - summary_message_count_threshold: config?.summaryMessageCountThreshold - }; -} - -async function get_context_summary_config(params?: { function_type?: string }) { - try { - const functionType = (params?.function_type ?? "CHAT").trim().toUpperCase(); - if (!functionType) { - complete({ - success: false, - message: "Missing required parameter: function_type" - }); - return; - } - - const result = (await Tools.SoftwareSettings.getFunctionModelConfig( - functionType - )) as FunctionModelConfigDetailLike; - const config = result.config; - if (!config) { - complete({ - success: false, - message: `No bound config found for function_type: ${functionType}`, - data: result - }); - return; - } - - complete({ - success: true, - message: `Context summary config fetched for ${functionType}.`, - data: { - function_type: functionType, - config_id: result.configId ?? config.id ?? "", - config_name: result.configName ?? config.name ?? "", - model_index: result.modelIndex ?? 0, - actual_model_index: result.actualModelIndex ?? 0, - selected_model: result.selectedModel ?? "", - context_summary: pick_context_summary_fields(config), - raw: result - } - }); - } catch (error: unknown) { - complete({ - success: false, - message: get_error_message(error) - }); - } -} - -async function set_context_summary_config(params?: { - function_type?: string; - context_length?: number; - max_context_length?: number; - enable_max_context_mode?: boolean; - summary_token_threshold?: number; - enable_summary?: boolean; - enable_summary_by_message_count?: boolean; - summary_message_count_threshold?: number; -}) { - try { - const functionType = (params?.function_type ?? "CHAT").trim().toUpperCase(); - if (!functionType) { - complete({ - success: false, - message: "Missing required parameter: function_type" - }); - return; - } - - const binding = (await Tools.SoftwareSettings.getFunctionModelConfig( - functionType - )) as FunctionModelConfigDetailLike; - const configId = (binding.configId ?? binding.config?.id ?? "").trim(); - if (!configId) { - complete({ - success: false, - message: `No bound config found for function_type: ${functionType}`, - data: binding - }); - return; - } - - const defaultUpdates = { - context_length: 48, - max_context_length: 128, - enable_max_context_mode: true, - summary_token_threshold: 0.7, - enable_summary: true, - enable_summary_by_message_count: true, - summary_message_count_threshold: 16 - }; - - const updates = { - ...defaultUpdates, - ...(params?.context_length === undefined ? {} : { context_length: params.context_length }), - ...(params?.max_context_length === undefined - ? {} - : { max_context_length: params.max_context_length }), - ...(params?.enable_max_context_mode === undefined - ? {} - : { enable_max_context_mode: params.enable_max_context_mode }), - ...(params?.summary_token_threshold === undefined - ? {} - : { summary_token_threshold: params.summary_token_threshold }), - ...(params?.enable_summary === undefined ? {} : { enable_summary: params.enable_summary }), - ...(params?.enable_summary_by_message_count === undefined - ? {} - : { enable_summary_by_message_count: params.enable_summary_by_message_count }), - ...(params?.summary_message_count_threshold === undefined - ? {} - : { summary_message_count_threshold: params.summary_message_count_threshold }) - }; - - const updateResult = (await Tools.SoftwareSettings.updateModelConfig( - configId, - updates - )) as ModelConfigUpdateResultLike; - - complete({ - success: true, - message: `Context summary config updated for ${functionType}.`, - data: { - function_type: functionType, - config_id: configId, - applied_updates: updates, - before: pick_context_summary_fields(binding.config), - after: pick_context_summary_fields(updateResult.config), - update_result: updateResult - } - }); - } catch (error: unknown) { - complete({ - success: false, - message: get_error_message(error) - }); - } -} - -async function set_function_model_config(params?: { - function_type?: string; - config_id?: string; - model_index?: number | string; -}) { - try { - const functionType = (params?.function_type ?? "").trim(); - const configId = (params?.config_id ?? "").trim(); - if (!functionType) { - complete({ - success: false, - message: "Missing required parameter: function_type" - }); - return; - } - if (!configId) { - complete({ - success: false, - message: "Missing required parameter: config_id" - }); - return; - } - - const result = await Tools.SoftwareSettings.setFunctionModelConfig( - functionType, - configId, - params?.model_index - ); - complete({ - success: true, - message: "Function model binding updated.", - data: result - }); - } catch (error: unknown) { - complete({ - success: false, - message: get_error_message(error) - }); - } -} - -async function test_model_config_connection(params?: { config_id?: string; model_index?: number | string }) { - try { - const configId = (params?.config_id ?? "").trim(); - if (!configId) { - complete({ - success: false, - message: "Missing required parameter: config_id" - }); - return; - } - - const result = await Tools.SoftwareSettings.testModelConfigConnection(configId, params?.model_index); - const success = !!result?.success; - complete({ - success, - message: success ? "Model config connection tests passed." : "Model config connection tests have failures.", - data: result - }); - } catch (error: unknown) { - complete({ - success: false, - message: get_error_message(error) - }); - } -} - -async function ping_mcp(params?: { package_name?: string }) { - try { - const packageName = (params?.package_name ?? "").trim(); - if (!packageName) { - complete({ - success: false, - message: "Missing required parameter: package_name" - }); - return; - } - - const result = await Tools.System.usePackage(packageName); - complete({ - success: true, - message: `Package probe finished: ${packageName}`, - data: result - }); - } catch (error: unknown) { - complete({ - success: false, - message: get_error_message(error) - }); - } -} - - return { - operit_editor, - how_make_skill, - list_sandbox_packages, - set_sandbox_package_enabled, - debug_install_js_package, - debug_install_toolpkg, - debug_run_sandbox_script, - read_environment_variable, - write_environment_variable, - restart_mcp_with_logs, - get_speech_services_config, - set_speech_services_config, - test_tts_playback, - list_model_configs, - create_model_config, - update_model_config, - delete_model_config, - list_function_model_configs, - get_function_model_config, - get_context_summary_config, - set_context_summary_config, - set_function_model_config, - test_model_config_connection, - ping_mcp - }; -})(); - -exports.operit_editor = operitEditorPackage.operit_editor; -exports.how_make_skill = operitEditorPackage.how_make_skill; -exports.list_sandbox_packages = operitEditorPackage.list_sandbox_packages; -exports.set_sandbox_package_enabled = operitEditorPackage.set_sandbox_package_enabled; -exports.debug_install_js_package = operitEditorPackage.debug_install_js_package; -exports.debug_install_toolpkg = operitEditorPackage.debug_install_toolpkg; -exports.debug_run_sandbox_script = operitEditorPackage.debug_run_sandbox_script; -exports.read_environment_variable = operitEditorPackage.read_environment_variable; -exports.write_environment_variable = operitEditorPackage.write_environment_variable; -exports.restart_mcp_with_logs = operitEditorPackage.restart_mcp_with_logs; -exports.get_speech_services_config = operitEditorPackage.get_speech_services_config; -exports.set_speech_services_config = operitEditorPackage.set_speech_services_config; -exports.test_tts_playback = operitEditorPackage.test_tts_playback; -exports.list_model_configs = operitEditorPackage.list_model_configs; -exports.create_model_config = operitEditorPackage.create_model_config; -exports.update_model_config = operitEditorPackage.update_model_config; -exports.delete_model_config = operitEditorPackage.delete_model_config; -exports.list_function_model_configs = operitEditorPackage.list_function_model_configs; -exports.get_function_model_config = operitEditorPackage.get_function_model_config; -exports.get_context_summary_config = operitEditorPackage.get_context_summary_config; -exports.set_context_summary_config = operitEditorPackage.set_context_summary_config; -exports.set_function_model_config = operitEditorPackage.set_function_model_config; -exports.test_model_config_connection = operitEditorPackage.test_model_config_connection; -exports.ping_mcp = operitEditorPackage.ping_mcp; - - - - diff --git a/scripts/pdf_vision_parser.ts b/scripts/pdf_vision_parser.ts deleted file mode 100644 index 4d0b3e3..0000000 --- a/scripts/pdf_vision_parser.ts +++ /dev/null @@ -1,755 +0,0 @@ -/* METADATA -{ - "name": "pdf_vision_parser", - "display_name": { - "zh": "PDF 逐页识图解析", - "en": "PDF Vision Parser" - }, - "description": { - "zh": "将 PDF 逐页渲染为图片,并调用用户当前配置的识图模型进行解析,最后按页拼接结果。", - "en": "Render a PDF page by page into images, send each image to the user's configured vision model, and merge the results." - }, - "enabledByDefault": false, - "category": "File", - "tools": [ - { - "name": "parse_pdf_with_vision", - "description": { - "zh": "将 PDF 逐页拆成图片,逐页调用 IMAGE_RECOGNITION 功能模型识图,并返回分页索引与输出文件路径。", - "en": "Split a PDF into page images, analyze each page with the IMAGE_RECOGNITION function model, and return per-page indexes plus output file paths." - }, - "parameters": [ - { - "name": "pdf_path", - "description": { - "zh": "PDF 文件路径。", - "en": "Path to the PDF file." - }, - "type": "string", - "required": true - }, - { - "name": "page_prompt", - "description": { - "zh": "每页识图提示词;不传则使用默认的尽量转文字提示词。", - "en": "Vision prompt for each page; defaults to a text-extraction-oriented prompt." - }, - "type": "string", - "required": false - }, - { - "name": "start_page", - "description": { - "zh": "起始页码,1-based,默认 1。", - "en": "Start page number, 1-based, default 1." - }, - "type": "number", - "required": false - }, - { - "name": "end_page", - "description": { - "zh": "结束页码,1-based,默认文档最后一页。", - "en": "End page number, 1-based, default is the last page." - }, - "type": "number", - "required": false - } - ] - } - ] -}*/ - -const PdfVisionParser = (function () { - const DEFAULT_PAGE_PROMPT = - "尽量完整提取本页可见文字与结构,保留标题、列表、表格顺序,不做总结,不补充页外信息;公式和图表转成可读文本描述"; - const TOOL_NAME = "parse_pdf_with_vision"; - const TOOL_FUNCTION_TYPE = "IMAGE_RECOGNITION"; - const RENDER_SCALE = 2; - const PAGE_CONCURRENCY = 4; - const IMAGE_FORMAT = "png"; - const MERGED_OUTPUT_FILE_NAME = "parsed_output.txt"; - - const File = Java.type("java.io.File"); - const FileOutputStream = Java.type("java.io.FileOutputStream"); - const OutputStreamWriter = Java.type("java.io.OutputStreamWriter"); - const BufferedWriter = Java.type("java.io.BufferedWriter"); - const ParcelFileDescriptor = Java.type("android.os.ParcelFileDescriptor"); - const PdfRenderer = Java.type("android.graphics.pdf.PdfRenderer"); - const PdfRendererPage = Java.type("android.graphics.pdf.PdfRenderer$Page"); - const Bitmap = Java.type("android.graphics.Bitmap"); - const BitmapConfig = Java.type("android.graphics.Bitmap$Config"); - const BitmapCompressFormat = Java.type("android.graphics.Bitmap$CompressFormat"); - const Color = Java.type("android.graphics.Color"); - const StandardCharsets = Java.type("java.nio.charset.StandardCharsets"); - const EnhancedAIService = Java.type("com.ai.assistance.operit.api.chat.EnhancedAIService"); - - interface ParsePdfWithVisionParams { - pdf_path: string; - page_prompt?: string; - start_page?: number; - end_page?: number; - } - - interface FailedPageInfo { - page_number: number; - stage: "precheck" | "render" | "vision"; - error: string; - } - - interface PageResult { - page_number: number; - image_path: string; - image_link: string; - output_path: string; - } - - interface InternalPageResult extends PageResult { - analysis: string; - } - - interface PageProcessingFailure { - page_number: number; - stage: FailedPageInfo["stage"]; - error: string; - partial_results: InternalPageResult[]; - } - - interface VisionModelInfo { - function_type: "IMAGE_RECOGNITION"; - config_id: string; - config_name: string; - selected_model: string; - } - - interface ParsePdfWithVisionData { - pdf_path: string; - image_dir: string; - output_path: string; - total_page_count: number; - processed_page_count: number; - pages: PageResult[]; - vision_model: VisionModelInfo; - failed_page?: FailedPageInfo; - } - - interface ToolResponse { - success: boolean; - message: string; - data?: ParsePdfWithVisionData; - } - - function isBlank(value: unknown): boolean { - return String(value ?? "").trim().length === 0; - } - - function toTrimmedString(value: unknown): string { - return String(value ?? "").trim(); - } - - function parsePositiveInteger( - value: number | undefined, - fieldName: string, - options?: { allowUndefined?: boolean } - ): number | undefined { - if (value === undefined || value === null) { - if (options?.allowUndefined) { - return undefined; - } - throw new Error(`${fieldName} 不能为空。`); - } - - if (!Number.isInteger(value) || value <= 0) { - throw new Error(`${fieldName} 必须是大于 0 的整数。`); - } - return value; - } - - function buildMergedText(pages: InternalPageResult[]): string { - return pages - .map(page => `===== Page ${page.page_number} =====\n${page.analysis}`) - .join("\n\n"); - } - - function buildVisionModelInfo(binding: FunctionModelConfigResultData): VisionModelInfo { - return { - function_type: TOOL_FUNCTION_TYPE, - config_id: String(binding.configId || ""), - config_name: String(binding.configName || ""), - selected_model: String(binding.selectedModel || "") - }; - } - - function buildData( - pdfPath: string, - imageDir: string, - outputPath: string, - totalPageCount: number, - pages: InternalPageResult[], - visionModel: VisionModelInfo, - failedPage?: FailedPageInfo - ): ParsePdfWithVisionData { - const data: ParsePdfWithVisionData = { - pdf_path: pdfPath, - image_dir: imageDir, - output_path: outputPath, - total_page_count: totalPageCount, - processed_page_count: pages.length, - pages: pages.map(page => ({ - page_number: page.page_number, - image_path: page.image_path, - image_link: page.image_link, - output_path: page.output_path - })), - vision_model: visionModel - }; - - if (failedPage) { - data.failed_page = failedPage; - } - - return data; - } - - function buildPrecheckFailure(message: string): ToolResponse { - return { - success: false, - message, - data: { - pdf_path: "", - image_dir: "", - output_path: "", - total_page_count: 0, - processed_page_count: 0, - pages: [], - vision_model: { - function_type: TOOL_FUNCTION_TYPE, - config_id: "", - config_name: "", - selected_model: "" - }, - failed_page: { - page_number: 0, - stage: "precheck", - error: message - } - } - }; - } - - function safeClose(resource: any, label: string): void { - if (!resource) { - return; - } - try { - resource.close(); - } catch (error) { - console.warn(`[${TOOL_NAME}] 关闭 ${label} 失败: ${String(error)}`); - } - } - - function safeRecycle(bitmap: any): void { - if (!bitmap) { - return; - } - try { - bitmap.recycle(); - } catch (error) { - console.warn(`[${TOOL_NAME}] 回收 bitmap 失败: ${String(error)}`); - } - } - - function ensureDirectoryExists(dir: any): void { - if (dir.exists()) { - if (!dir.isDirectory()) { - throw new Error(`目标路径不是目录: ${String(dir.getAbsolutePath())}`); - } - return; - } - const created = dir.mkdirs(); - if (!created) { - throw new Error(`无法创建目录: ${String(dir.getAbsolutePath())}`); - } - } - - function resolveAppContext(): JavaBridgeInstance { - if (typeof Java.getApplicationContext === "function") { - return Java.getApplicationContext(); - } - if (typeof Java.getContext === "function") { - return Java.getContext(); - } - throw new Error("无法获取应用 Context。"); - } - - function createOutputDirectory(): any { - const parserRootDir = new File(OPERIT_CLEAN_ON_EXIT_DIR, "pdf_vision_parser"); - ensureDirectoryExists(parserRootDir); - - const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); - const runDir = new File(parserRootDir, timestamp); - ensureDirectoryExists(runDir); - return runDir; - } - - function writeTextFile(targetFile: any, content: string): string { - let outputStream: any = null; - let writer: any = null; - let bufferedWriter: any = null; - try { - outputStream = new FileOutputStream(targetFile); - writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8); - bufferedWriter = new BufferedWriter(writer); - bufferedWriter.write(String(content ?? "")); - bufferedWriter.flush(); - return String(targetFile.getAbsolutePath()); - } finally { - safeClose(bufferedWriter, "bufferedWriter"); - safeClose(writer, "writer"); - safeClose(outputStream, "outputStream"); - } - } - - function writePageOutputFile(outputDir: any, pageNumber: number, content: string): string { - const textFile = new File(outputDir, `page-${padPageNumber(pageNumber)}.txt`); - return writeTextFile(textFile, content); - } - - function writeMergedOutputFile(outputDir: any, pages: InternalPageResult[]): string { - if (!outputDir) { - return ""; - } - const mergedFile = new File(outputDir, MERGED_OUTPUT_FILE_NAME); - return writeTextFile(mergedFile, buildMergedText(pages)); - } - - function padPageNumber(pageNumber: number): string { - return String(pageNumber).padStart(4, "0"); - } - - function assertVisionBinding(binding: FunctionModelConfigResultData): void { - if (!binding || isBlank(binding.configId)) { - throw new Error("IMAGE_RECOGNITION 当前没有可用绑定。"); - } - if (!binding.config || binding.config.enableDirectImageProcessing !== true) { - throw new Error("IMAGE_RECOGNITION 当前绑定的模型未启用识图能力。"); - } - } - - function validatePdfFile(pdfPath: string): any { - if (isBlank(pdfPath)) { - throw new Error("pdf_path 不能为空。"); - } - - const pdfFile = new File(pdfPath); - if (!pdfFile.exists() || !pdfFile.isFile()) { - throw new Error(`PDF 文件不存在: ${pdfPath}`); - } - - if (!String(pdfFile.getName()).toLowerCase().endsWith(".pdf")) { - throw new Error(`文件不是 PDF: ${pdfPath}`); - } - - return pdfFile; - } - - function stripThinkingContent(content: string): string { - return String(content ?? "") - .replace(/[\s\S]*?(<\/think(?:ing)?>|\z)/gi, "") - .replace(/[\s\S]*?(<\/search>|\z)/gi, "") - .trim(); - } - - function sortPageResults(pages: InternalPageResult[]): InternalPageResult[] { - return [...pages].sort((left, right) => left.page_number - right.page_number); - } - - function normalizeErrorMessage(error: unknown): string { - return String(error instanceof Error ? error.message : error); - } - - async function analyzePageImage( - service: JavaBridgeInstance, - imagePath: string, - prompt: string - ): Promise { - const rawAnalysis = String( - await service.callSuspend("analyzeImageWithIntent", imagePath, prompt) - ).trim(); - const analysis = stripThinkingContent(rawAnalysis); - - if (analysis.length === 0) { - throw new Error("识图模型返回了空结果。"); - } - if (/^Image recognition failed:/i.test(rawAnalysis) || /^Image recognition failed:/i.test(analysis)) { - throw new Error(rawAnalysis || analysis); - } - - return analysis; - } - - async function processSinglePage( - pdfFile: any, - outputDir: any, - pageNumber: number, - prompt: string, - service: JavaBridgeInstance - ): Promise { - console.log(`[${TOOL_NAME}] 开始处理第 ${pageNumber} 页`); - - const zeroBasedPageIndex = pageNumber - 1; - const imageFile = new File( - outputDir, - `page-${padPageNumber(pageNumber)}.${IMAGE_FORMAT}` - ); - const imagePath = String(imageFile.getAbsolutePath()); - - let fileDescriptor: any = null; - let pdfRenderer: any = null; - let page: any = null; - let bitmap: any = null; - let outputStream: any = null; - let stage: FailedPageInfo["stage"] = "render"; - - try { - fileDescriptor = ParcelFileDescriptor.open( - pdfFile, - ParcelFileDescriptor.MODE_READ_ONLY - ); - pdfRenderer = new PdfRenderer(fileDescriptor); - page = pdfRenderer.openPage(zeroBasedPageIndex); - - const width = Number(page.getWidth()) * RENDER_SCALE; - const height = Number(page.getHeight()) * RENDER_SCALE; - - bitmap = Bitmap.createBitmap(width, height, BitmapConfig.ARGB_8888); - bitmap.eraseColor(Color.WHITE); - page.render( - bitmap, - null, - null, - PdfRendererPage.RENDER_MODE_FOR_DISPLAY - ); - - outputStream = new FileOutputStream(imageFile); - const compressed = bitmap.compress( - BitmapCompressFormat.PNG, - 100, - outputStream - ); - if (!compressed) { - throw new Error(`页面 ${pageNumber} 图片写入失败。`); - } - - safeClose(outputStream, `page-${pageNumber} output stream`); - outputStream = null; - - const imageLink = String( - NativeInterface.registerImageFromPath(imagePath) || "" - ).trim(); - if (!imageLink) { - throw new Error(`页面 ${pageNumber} 图片链接注册失败。`); - } - - stage = "vision"; - const analysis = await analyzePageImage(service, imagePath, prompt); - const pageOutputPath = writePageOutputFile(outputDir, pageNumber, analysis); - - console.log(`[${TOOL_NAME}] 第 ${pageNumber} 页处理完成`); - return { - page_number: pageNumber, - image_path: imagePath, - image_link: imageLink, - output_path: pageOutputPath, - analysis - }; - } catch (error) { - throw { - page_number: pageNumber, - stage, - error: normalizeErrorMessage(error), - partial_results: [] - } as PageProcessingFailure; - } finally { - safeClose(outputStream, `page-${pageNumber} output stream`); - safeRecycle(bitmap); - safeClose(page, `page-${pageNumber}`); - safeClose(pdfRenderer, `page-${pageNumber} pdfRenderer`); - safeClose(fileDescriptor, `page-${pageNumber} parcelFileDescriptor`); - } - } - - async function processPagesWithConcurrency( - pageNumbers: number[], - limit: number, - worker: (pageNumber: number) => Promise - ): Promise { - const results: Array = new Array(pageNumbers.length); - let nextIndex = 0; - let failure: Omit | null = null; - - async function runWorker(): Promise { - while (true) { - if (failure) { - return; - } - - const currentIndex = nextIndex; - nextIndex += 1; - - if (currentIndex >= pageNumbers.length) { - return; - } - - const pageNumber = pageNumbers[currentIndex]; - try { - results[currentIndex] = await worker(pageNumber); - } catch (error) { - if (!failure) { - const errorData = error as Partial | undefined; - failure = { - page_number: Number(errorData?.page_number ?? pageNumber), - stage: (errorData?.stage as FailedPageInfo["stage"] | undefined) ?? "render", - error: normalizeErrorMessage(errorData?.error ?? error) - }; - } - return; - } - } - } - - const workerCount = Math.max(1, Math.min(limit, pageNumbers.length)); - await Promise.all( - Array.from({ length: workerCount }, () => runWorker()) - ); - - const partialResults = sortPageResults( - results.filter((page): page is InternalPageResult => Boolean(page)) - ); - if (failure) { - const failureInfo = failure as Omit; - throw { - page_number: failureInfo.page_number, - stage: failureInfo.stage, - error: failureInfo.error, - partial_results: partialResults - } as PageProcessingFailure; - } - - return partialResults; - } - - async function parsePdfWithVisionInternal( - params: ParsePdfWithVisionParams - ): Promise { - let binding: FunctionModelConfigResultData; - try { - binding = await Tools.SoftwareSettings.getFunctionModelConfig(TOOL_FUNCTION_TYPE); - assertVisionBinding(binding); - } catch (error) { - const message = String(error instanceof Error ? error.message : error); - return buildPrecheckFailure(message); - } - - const visionModel = buildVisionModelInfo(binding); - const rawPdfPath = toTrimmedString(params?.pdf_path); - - let pdfFile: any; - let startPage: number | undefined; - let endPageInput: number | undefined; - const resolvedPrompt = - toTrimmedString(params?.page_prompt) || DEFAULT_PAGE_PROMPT; - - try { - pdfFile = validatePdfFile(rawPdfPath); - startPage = parsePositiveInteger(params?.start_page, "start_page", { - allowUndefined: true - }) ?? 1; - endPageInput = parsePositiveInteger(params?.end_page, "end_page", { - allowUndefined: true - }); - } catch (error) { - const message = String(error instanceof Error ? error.message : error); - return { - success: false, - message, - data: { - pdf_path: rawPdfPath, - image_dir: "", - output_path: "", - total_page_count: 0, - processed_page_count: 0, - pages: [], - vision_model: visionModel, - failed_page: { - page_number: 0, - stage: "precheck", - error: message - } - } - }; - } - - const pages: InternalPageResult[] = []; - const context = resolveAppContext(); - const enhancedAiService = EnhancedAIService.getInstance(context) as JavaBridgeInstance; - let outputDir: any = null; - let imageDir = ""; - let outputPath = ""; - - let fileDescriptor: any = null; - let pdfRenderer: any = null; - let totalPageCount = 0; - - try { - fileDescriptor = ParcelFileDescriptor.open( - pdfFile, - ParcelFileDescriptor.MODE_READ_ONLY - ); - pdfRenderer = new PdfRenderer(fileDescriptor); - totalPageCount = Number(pdfRenderer.getPageCount()); - - if (!Number.isInteger(totalPageCount) || totalPageCount <= 0) { - const error = "PDF 没有可处理的页面。"; - return { - success: false, - message: error, - data: buildData(rawPdfPath, imageDir, outputPath, 0, pages, visionModel, { - page_number: 0, - stage: "render", - error - }) - }; - } - - const resolvedEndPage = Math.min(endPageInput ?? totalPageCount, totalPageCount); - if ((startPage ?? 1) > resolvedEndPage) { - const error = - (startPage ?? 1) > totalPageCount - ? `start_page 超出了总页数,文档总页数为 ${totalPageCount}。` - : "start_page 不能大于 end_page。"; - return { - success: false, - message: error, - data: buildData(rawPdfPath, imageDir, outputPath, totalPageCount, pages, visionModel, { - page_number: 0, - stage: "precheck", - error - }) - }; - } - - outputDir = createOutputDirectory(); - imageDir = String(outputDir.getAbsolutePath()); - const pageNumbers: number[] = []; - for (let pageNumber = startPage ?? 1; pageNumber <= resolvedEndPage; pageNumber += 1) { - pageNumbers.push(pageNumber); - } - - try { - const processedPages = await processPagesWithConcurrency( - pageNumbers, - PAGE_CONCURRENCY, - currentPageNumber => - processSinglePage( - pdfFile, - outputDir, - currentPageNumber, - resolvedPrompt, - enhancedAiService - ) - ); - pages.push(...processedPages); - } catch (error) { - const failedPage = error as PageProcessingFailure; - pages.push(...sortPageResults(failedPage.partial_results ?? [])); - outputPath = writeMergedOutputFile(outputDir, pages); - - return { - success: false, - message: `第 ${failedPage.page_number} 页处理失败: ${failedPage.error}`, - data: buildData(rawPdfPath, imageDir, outputPath, totalPageCount, pages, visionModel, { - page_number: failedPage.page_number, - stage: failedPage.stage, - error: failedPage.error - }) - }; - } - - outputPath = writeMergedOutputFile(outputDir, pages); - return { - success: true, - message: `成功解析 ${pages.length} 页 PDF 并完成识图,结果已写入 ${outputPath}。`, - data: buildData(rawPdfPath, imageDir, outputPath, totalPageCount, pages, visionModel) - }; - } catch (error) { - const message = String(error instanceof Error ? error.message : error); - outputPath = writeMergedOutputFile(outputDir, pages); - return { - success: false, - message, - data: buildData(rawPdfPath, imageDir, outputPath, totalPageCount, pages, visionModel, { - page_number: 0, - stage: "render", - error: message - }) - }; - } finally { - safeClose(pdfRenderer, "pdfRenderer"); - safeClose(fileDescriptor, "parcelFileDescriptor"); - } - } - - async function wrapToolExecution( - func: (params: ParsePdfWithVisionParams) => Promise, - params: ParsePdfWithVisionParams - ): Promise { - try { - const result = await func(params || ({} as ParsePdfWithVisionParams)); - complete(result); - } catch (error) { - const message = String(error instanceof Error ? error.message : error); - complete({ - success: false, - message, - data: { - pdf_path: toTrimmedString(params?.pdf_path), - image_dir: "", - output_path: "", - total_page_count: 0, - processed_page_count: 0, - pages: [], - vision_model: { - function_type: TOOL_FUNCTION_TYPE, - config_id: "", - config_name: "", - selected_model: "" - }, - failed_page: { - page_number: 0, - stage: "precheck", - error: message - } - } - }); - } - } - - async function parse_pdf_with_vision( - params: ParsePdfWithVisionParams - ): Promise { - await wrapToolExecution(parsePdfWithVisionInternal, params); - } - - async function main(params: ParsePdfWithVisionParams): Promise { - await wrapToolExecution(parsePdfWithVisionInternal, params); - } - - return { - parse_pdf_with_vision, - main - }; -})(); - -exports.parse_pdf_with_vision = PdfVisionParser.parse_pdf_with_vision; -exports.main = PdfVisionParser.main; diff --git a/scripts/qq_intelligent.ts b/scripts/qq_intelligent.ts deleted file mode 100644 index 6fd95b8..0000000 --- a/scripts/qq_intelligent.ts +++ /dev/null @@ -1,410 +0,0 @@ -/* -METADATA -{ - // QQ Intelligent Assistant Package - name: Experimental_qq_intelligent - - display_name: { - zh: "实验性 QQ 智能助手" - en: "Experimental QQ Intelligent Assistant" - }description: { zh: "高级QQ智能助手,通过UI自动化技术实现QQ应用交互,支持消息自动回复、历史记录读取、联系人搜索与通讯等功能,为AI赋予QQ社交能力。适用于智能客服、自动回复、社交辅助等场景。", en: "Advanced QQ assistant powered by UI automation. Supports sending messages, reading chat history, searching contacts/groups, and basic communication workflows, enabling AI-driven social messaging scenarios." } - - // Tools in this package - category: "Media" - tools: [ - { - name: reply - description: { zh: "在当前聊天窗口输入消息并发送。一般情况下,用户想要ai帮忙发送消息时,需要ai自己去生成回复的消息,如果不确定发送的内容,请不要调用工具。只要是停留在聊天界面,就可以直接调用这个。", en: "Type and send a message in the current chat window. Usually the AI should generate the message content by itself; if you are unsure what to send, do not call this tool. As long as you are staying on a chat page, you can call it directly." } - parameters: [ - { - name: message - description: { zh: "要发送的消息", en: "Message text to send." } - type: string - required: true - }, - { - name: click_send - description: { zh: "是否点击发送按钮", en: "Whether to tap the Send button." } - type: boolean - required: false - } - ] - }, - { - name: find_user - description: { zh: "在QQ联系人或群成员中查找用户", en: "Find a user in QQ contacts or group chats." } - parameters: [ - { - name: user_name - description: { zh: "搜索用户名称", en: "User name keyword to search." } - type: string - required: true - }, - { - name: user_type - description: { zh: "搜索类型(contacts/groups)", en: "Search type: contacts or groups." } - type: string - required: true - } - ] - }, - { - name: find_and_reply - description: { zh: "查找用户并发送消息", en: "Find a user and send a message." } - parameters: [ - { - name: message - description: { zh: "要发送的消息", en: "Message text to send." } - type: string - required: true - }, - { - name: user_name - description: { zh: "发送目标用户名称", en: "Target user name keyword." } - type: string - required: true - }, - { - name: user_type - description: { zh: "用户类型(contacts/groups)", en: "Target type: contacts or groups." } - type: string - required: true - }, - { - name: click_send - description: { zh: "是否点击发送按钮", en: "Whether to tap the Send button." } - type: boolean - required: false - } - ] - }, - { - name: get_history - description: { zh: "获取当前聊天窗口的历史消息。如果用户要求读取聊天记录,那么可以用这个工具或者find_and_get_history。", en: "Get chat history from the current chat window. If the user asks to read chat history, you can use this tool or `find_and_get_history`." } - parameters: [ - { - name: message_num - description: { zh: "获取的消息数量", en: "Number of messages to retrieve." } - type: number - required: false - } - ] - }, - { - name: find_and_get_history - description: { zh: "查找用户并获取聊天历史记录。一般情况下,用户想要让ai帮忙回消息的时候,需要先调用这个,得到历史记录后直接调用reply。", en: "Find a user and retrieve chat history. Typically, when the user wants the AI to reply, call this first to get context, then call `reply`." } - parameters: [ - { - name: user_name - description: { zh: "搜索用户名称", en: "User name keyword to search." } - type: string - required: true - }, - { - name: user_type - description: { zh: "搜索类型(contacts/groups)", en: "Search type: contacts or groups." } - type: string - required: true - }, - { - name: message_num - description: { zh: "获取的消息数量", en: "Number of messages to retrieve." } - type: number - required: false - } - ] - } - ] -}*/ - -interface Array { - at(index: number): T | undefined; -} - -const QQIntelligent = (function () { - async function close_keyboard() { - await Tools.UI.pressKey("KEYCODE_BACK"); - } - - async function is_in_group() { - let page = await UINode.getCurrentPage(); - return page.findAllByContentDesc('语音').findIndex(e => e.className?.includes('ImageButton') && (e.centerPoint?.y ?? 0) > 1000) === -1; - } - - - - - Array.prototype.at = function (index: number) { - if (index < 0) { - index = this.length + index; - } - return this[index]; - } - - async function get_history(params: { message_num: number | string }) { - const message_num = Number(params.message_num) || 10; - let page = await UINode.getCurrentPage(); - - //获取群名称 - const chat_title = page.findById('com.tencent.mobileqq:id/ivTitleBtnLeft')?.parent?.allTexts()[0] ?? ""; - // const is_group = await is_in_group(); - console.log("chat_title", chat_title); - // console.log("is_group", is_group); - //先滑动到最底部 - let tryMax = 0; - while (tryMax < 1) { - tryMax++; - await Tools.UI.swipe(100, 1000, 100, 200); - await Tools.System.sleep(500); - } - - let messageList: { message: string, sender: string }[] = []; - let allMessages: { message: string, sender: string }[] = []; - - //获取历史消息 - tryMax = 0; - while (tryMax < 5) { - tryMax++; - - page = await UINode.getCurrentPage(); - console.log("page", page.toFormattedString!()); - const list_view = page.findByClass('RecyclerView'); - if (!list_view) { - return undefined; - } - - // 清空临时消息列表 - messageList = []; - - // 获取当前可见的消息列表 - for (const message of list_view.children) { - let message_text = message.allTexts().join('/'); - - let sender = "other"; - //判断头像再左边还是右边 - const avatar = message.findByClass('ImageView'); - if (avatar) { - if ((avatar.centerPoint?.x ?? 0) < 300) { - sender = "other"; - console.log("other", avatar.centerPoint?.x); - } else { - sender = "self"; - console.log("self", avatar.centerPoint?.x); - } - } else { - sender = "self"; - console.log("self"); - } - messageList.push({ message: message_text, sender: sender }); - } - - // 将当前视图中的消息添加到总集合中,保持顺序 - // 从下往上滑动时,将新获取的消息放在前面(保持旧消息在前,新消息在后的顺序) - allMessages = [...messageList, ...allMessages]; - - // 如果已经获取了足够多的消息,停止滚动 - if (allMessages.length >= message_num) { - break; - } - await Tools.UI.swipe(100, 300, 100, 1000); - await Tools.System.sleep(500); - } - - // 删除重复的消息,但保持原始顺序 - const uniqueMessages: { message: string, sender: string }[] = []; - const seen = new Set(); - - for (const msg of allMessages) { - const key = `${msg.message}-${msg.sender}`; - if (!seen.has(key)) { - seen.add(key); - uniqueMessages.push(msg); - } - } - - // 返回符合数量要求的消息列表(保持原始顺序) - return { messages: uniqueMessages.slice(0, message_num), chat_title: chat_title }; - } - - async function reply(params: { message: string, click_send: boolean }) { - // 提取参数 - const message = params.message || ""; - const click_send = params.click_send || false; - await Tools.UI.setText(message); - await Tools.System.sleep(500); - - if (click_send) { - await Tools.UI.clickElement({ - resourceId: "com.tencent.mobileqq:id/send_btn", - index: "0" - }); - } - return true; - } - - async function find_and_reply(params: { message: string, user_name: string, user_type: "contacts" | "groups", click_send: boolean }) { - // 提取参数 - const message = params.message || ""; - const user_name = params.user_name || ""; - const user_type = params.user_type || "contacts"; - const click_send = params.click_send || false; - let result = await find_user({ user_name: user_name, user_type: user_type }); - if (!result) { - return false; - } - await Tools.System.sleep(1000); - return await reply({ message: message, click_send: click_send }); - } - - async function ensureActivity(activityName: string = "", packageName: string = "com.tencent.mobileqq", enterActivity: () => Promise = async () => true, tryMax: number = 1) { - let android = new Android(); - let activity = await Tools.UI.getPageInfo(); - if (activity.activityName?.includes(activityName)) { - return true; - } - - while (tryMax > 0) { - tryMax--; - await Tools.System.stopApp(packageName); - await Tools.System.sleep(2000); - await Tools.System.startApp(packageName); - // let intent = android.createIntent(); - // intent.setComponent(packageName, activityName); - // await intent.start(); - await Tools.System.sleep(3000); // Give some time for app to launch - if (await enterActivity()) { - activity = await Tools.UI.getPageInfo(); - if (activity.activityName?.includes(activityName)) { - return true; - } - } - } - return false; - } - - async function find_user(params: { user_name: string, user_type: "contacts" | "groups" }) { - // 提取参数 - const user_name = params.user_name || ""; - const user_type = params.user_type || "contacts"; - - if (!await ensureActivity("com.tencent.mobileqq.search.activity.UniteSearchActivity", "com.tencent.mobileqq", async () => { - const search_btn = (await UINode.getCurrentPage()).findByText("搜索"); - if (search_btn) { - await search_btn.click(); - return true; - } - return false; - })) { - return false; - } - - let firstTarget: UINode | undefined = undefined; - let tryMax = 0; - while (tryMax < 2) { - tryMax++; - const search_btn = (await UINode.getCurrentPage()).findByText("搜索"); - if (search_btn) { - await search_btn.click(); - await Tools.System.sleep(500); - } - // 输入搜索内容 - await Tools.UI.setText(user_name); - await Tools.System.sleep(3000 * tryMax); - await close_keyboard(); - - const currentPage = await UINode.getCurrentPage(); - const searchResult = currentPage.findAllById('com.tencent.mobileqq:id/title'); - - let isNeedToCatch = false; - for (const child of searchResult!) { - if (isNeedToCatch) { - firstTarget = child; - break; - } - // const title = child.findById('com.tencent.mobileqq:id/title'); - const title = child; - if (title) { - if (user_type == "contacts" && title.text == "联系人") { - isNeedToCatch = true; - } else if (user_type == "groups" && title.text == "群聊") { - isNeedToCatch = true; - } - } - } - - if (firstTarget) { - await firstTarget.click(); - return true; - } - } - - return false; - } - - async function find_and_get_history(params: { user_name: string, user_type: "contacts" | "groups", message_num: number }) { - const user_name = params.user_name || ""; - const user_type = params.user_type || "contacts"; - const message_num = params.message_num || 10; - let result = await find_user({ user_name: user_name, user_type: user_type }); - if (!result) { - return undefined; - } - await Tools.System.sleep(1000); - return await get_history({ message_num: message_num }); - } - - async function main() { - // let result = await find_and_reply({ message: "你好你好!我是OPERIT,很高兴认识你!", user_name: "韩韩韩", user_type: "contacts", click_send: true }); - // await find_user({ user_name: "Wind", user_type: "contacts" }); - // await find_user({ user_name: "Dec", user_type: "groups" }); - // await Tools.System.sleep(1000); - let result = await find_and_get_history({ user_name: "Dec", user_type: "groups", message_num: 20 }); - console.log(result); - complete({ - success: result - }); - } - - async function wrap_bool(func: (params: any) => Promise, params: any, successMessage: string, failMessage: string, additionalMessage: string = "") { - if (await func(params)) { - complete({ - success: true, - message: successMessage, - additionalMessage: additionalMessage - }) - } else { - complete({ - success: false, - message: failMessage, - additionalMessage: additionalMessage - }) - } - } - - async function wrap_data(func: (params: any) => Promise, params: any, successMessage: string, failMessage: string, additionalMessage: string = "") { - const result = await func(params); - complete({ - success: true, - message: successMessage, - additionalMessage: additionalMessage, - data: result - }) - } - - return { - main: main, - reply: async (params) => await wrap_bool(reply, params, "发送成功", "发送失败"), - find_user: async (params) => await wrap_bool(find_user, params, "查找成功", "查找失败,停留在界面", (await UINode.getCurrentPage()).toFormattedString!()), - find_and_reply: async (params) => await wrap_bool(find_and_reply, params, "发送成功", "发送失败,停留在界面", (await UINode.getCurrentPage()).toFormattedString!()), - get_history: async (params) => await wrap_data(get_history, params, "获取历史消息成功", "获取历史消息失败"), - find_and_get_history: async (params) => await wrap_data(find_and_get_history, params, "获取历史消息成功", "获取历史消息失败") - } -})(); - -//逐个导出 -exports.reply = QQIntelligent.reply; -exports.find_user = QQIntelligent.find_user; -exports.find_and_reply = QQIntelligent.find_and_reply; -exports.get_history = QQIntelligent.get_history; -exports.find_and_get_history = QQIntelligent.find_and_get_history; -exports.main = QQIntelligent.main; \ No newline at end of file diff --git a/scripts/quick_start.ts b/scripts/quick_start.ts deleted file mode 100644 index af89281..0000000 --- a/scripts/quick_start.ts +++ /dev/null @@ -1,506 +0,0 @@ -/* - 欢迎来到 TypeScript/JavaScript AI 工具开发终极入门教程 (v2.0 详细版) - ==================================================================== - - 本教程为初学者设计,特别是那些熟悉其他脚本语言(如Python)但对TypeScript/JavaScript不熟悉的开发者。 - 我们将从最基础的语法开始,一步步带你构建一个功能完整、结构优雅的AI工具,并详细解释每一步背后的“为什么”。 -*/ - - -// ================================================================= -// Part 0: 关于运行环境 (A Note on the Environment) -// ================================================================= -/* - 在开始之前,你需要知道,你的代码并不是在真空中运行。它在一个特殊的“沙箱”环境中执行。 - 这个环境为你预先提供了一些全局可用的函数和对象,最重要的有: - - 1. `complete(result: object)`: - - 这是你的工具与AI系统沟通的【唯一】桥梁。 - - 当你的工具执行完毕,无论成功还是失败,都必须调用这个函数来返回结果。 - - AI会根据你传入这个函数的对象内容,来决定下一步的行为。 - - 2. `exports`: - - 这是一个特殊的对象,你可以把它看作是你代码文件的“公开接口”。 - - 你需要将你的工具函数“挂载”到 `exports` 对象上,AI才能找到并调用它们。 - - 示例: `exports.myToolName = myToolFunction;` - - 3. `Tools`: - - 这是一个内置的工具集,提供了很多实用的辅助功能,比如文件读写、网络请求、系统命令等。 - - 我们在本教程中会用到 `Tools.System.sleep()` 来演示异步操作。 - - 本教程的所有代码都在一个TypeScript文件(.ts)中。TypeScript是JavaScript的超集,它增加了“类型系统”, - 能帮助我们编写更健壮、更不容易出错的代码。 -*/ - - -// ================================================================= -// Part 1: JavaScript / TypeScript 核心语法快速入门 -// ================================================================= -/* - 本部分将详细介绍编写AI工具必需的JS/TS语法,并为每个知识点提供代码示例。 -*/ - -// --- 1.1 变量声明 (Variables) --- -/* - - `const` (常量): 用于声明一个值不会再改变的变量。这是首选的声明方式,能让代码更可预测。 - - `let` (变量): 用于声明一个值将来可能需要被修改的变量。 -*/ -function variable_example() { - const toolName = "My Greeter Tool"; // 一旦设定,toolName就不能再被赋值 - let executionCount = 0; // executionCount 的值可以改变 - console.log(`工具名称: ${toolName}`); - - executionCount = executionCount + 1; - console.log(`这是第 ${executionCount} 次执行。`); - - // 下面这行代码会报错,因为 toolName 是一个常量 - // toolName = "New Name"; -} -// variable_example(); // 你可以取消注释来在本地测试这些函数 - - -// --- 1.2 数据类型 (Data Types) --- -/* - TypeScript 强制我们思考数据的类型,这极大地减少了运行时错误。 - - `string`: 文本字符串。 - - `number`: 数字(整数或浮点数)。 - - `boolean`: 布尔值 (`true` 或 `false`)。 - - `object`: 对象,键值对的集合,类似于Python的字典。 - - `array`: 数组,有序的数据列表,类似于Python的列表。 - - `any`: 特殊类型,表示“任何类型都可以”。应尽量避免使用,因为它会削弱TypeScript的优势。 -*/ -function data_types_example() { - // 我们可以明确地为变量注解类型 - const userName: string = "Alice"; - const userAge: number = 30; - const isActive: boolean = true; - - // 对象类型注解,描述了一个对象的“形状” - const userInfo: { name: string; age: number; premium: boolean } = { - name: "Bob", - age: 25, - premium: false - }; - console.log(`${userInfo.name} 的年龄是 ${userInfo.age}`); - - // 数组类型注解,`string[]` 表示一个“只包含字符串的数组” - const permissions: string[] = ["read", "write", "execute"]; - console.log(`用户的第一个权限是: ${permissions[0]}`); - - // 遍历数组 - permissions.forEach(permission => { - console.log(`权限: ${permission}`); - }); -} -// data_types_example(); - - -// --- 1.3 函数 (Functions) 与 异步操作 (Async Operations) --- -/* - - `async function`: 这是定义AI工具函数的标准方式。`async` 关键字表明该函数内部可能包含需要“等待”的操作。 - - `await`: 必须在 `async` 函数内部使用。它会暂停函数的执行,直到其后的异步操作(通常是一个`Promise`)完成,然后返回结果。 - - `Promise`: 代表一个异步操作最终完成(或失败)的结果。`async` 函数的返回值总是一个`Promise`。 - 例如 `Promise` 意味着这个异步函数最终会返回一个字符串。 -*/ -async function async_function_example(name: string): Promise { - console.log(`开始向 ${name} 发送问候...`); - - // `await` 在这里暂停函数,等待 Tools.System.sleep(1500) 完成 - // 这模拟了一个耗时1.5秒的网络请求或文件操作 - await Tools.System.sleep(1500); - - const greetingMessage = `你好, ${name}! 异步问候已送达。`; - console.log(greetingMessage); - - // async 函数通过 return 返回的值,会被包装在 Promise 中 - return greetingMessage; -} -// async_function_example("Charlie"); - - -// --- 1.4 错误处理 (Error Handling) --- -/* - - `try...catch`: 这是处理潜在错误的标准方式,对于健壮的工具至关重要。 - - `throw new Error(...)`: 当函数执行不下去时,主动“抛出”一个错误,中断当前`try`块的执行,并被`catch`块捕获。 -*/ -function error_handling_example(input: any) { - try { - console.log("尝试处理输入..."); - - // 步骤1: 检查输入是否为非空字符串 - if (typeof input !== 'string' || input.trim() === '') { - // 如果检查失败,就“抛出”一个错误对象 - throw new Error("输入必须是一个非空的字符串。"); - } - - // 步骤2: 如果代码能执行到这里,说明没有错误发生 - console.log(`输入有效,内容是: "${input}"`); - return { success: true, data: input }; - - } catch (error) { - // 步骤3: 如果`try`块中任何地方抛出了错误,程序会立即跳转到这里 - console.error("发生了一个错误!"); - console.error(`错误详情: ${error.message}`); // error.message 包含了我们抛出时提供的信息 - - // 在实际工具中,这里我们会调用 complete() 来报告失败 - return { success: false, message: error.message }; - } -} -// error_handling_example("这是一个有效的输入"); -// error_handling_example(""); // 这个会触发错误 -// error_handling_example(123); // 这个也会触发错误 - - -// ================================================================= -// Part 2: AI工具的本质 与 `complete()` 函数 -// ================================================================= -/* - 如前所述,一个AI工具就是一个函数,它通过 `complete()` 函数与AI系统交互。 - 下面是一个最基础的、结构完整的AI工具示例,我们为其中的关键部分添加了注释。 -*/ -async function simple_greeter_tool(params: { user_name: string }) { - // 典型的工具函数总是在一个大的 try...catch 块中 - try { - // 1. 从 AI 接收到的参数中解构出需要的值 - const { user_name } = params; - - // 2. 参数校验:永远不要相信输入! - if (!user_name) { - throw new Error("参数 'user_name' 缺失,无法生成问候语。"); - } - - // 3. 执行核心业务逻辑 - const greeting = `你好, ${user_name}! 欢迎来到AI工具的世界。`; - - // 4. 调用 complete(),报告成功 - // - success: true 表示工具成功完成了它的任务。 - // - data: 存放工具的执行结果,这是AI最关心的部分。 - complete({ - success: true, - data: greeting - }); - - } catch (error) { - // 5. 如果 try 块中任何地方出错(无论是参数校验还是业务逻辑),都会进入这里 - console.error("simple_greeter_tool 执行失败:", error); - - // 6. 调用 complete(),报告失败 - // - success: false 告诉AI任务失败了。 - // - message: 存放清晰的错误信息,帮助AI理解失败原因。 - complete({ - success: false, - message: `工具执行失败: ${error.message}` - }); - } -} - - -// ================================================================= -// Part 3: Metadata 和 函数导出 - 连接AI与代码的桥梁 -// ================================================================= -/* - 我们已经写好了工具函数,但AI如何知道它的存在、功能和用法呢? - 这需要两样东西: - 1. `METADATA` (元数据): 就像是工具的“说明书”。 - 2. `exports` (导出): 像是把工具函数“注册”到系统里。 - - --- 3.1 详解 METADATA --- - 文件顶部的 ` METADATA ... ` 注释块就是这份说明书。AI在加载时会解析它。 - - `name`: (string) 工具集的唯一ID。 - - `description`: (string) 对整个工具集的详细描述。 - - `tools`: (array) 一个数组,包含此文件中所有可用的工具。 - - `name`: (string) 单个工具的函数名。这个名字【必须】和后面 `exports` 中使用的名字完全一致。 - - `description`: (string) 对这个工具能做什么的清晰描述。AI会根据这个描述来决定何时调用它。 - - `parameters`: (array) 定义了此工具需要哪些参数。 - - `name`: (string) 参数名。 - - `description`: (string) 对参数用途的描述。 - - `type`: (string) 参数的类型 (e.g., 'string', 'number', 'boolean')。 - - `required`: (boolean) 这个参数是否是必需的。 -*/ - -/* METADATA -{ - "name": "greeter_tool_v2", - - "display_name": { - "zh": "问候工具 V2", - "en": "Greeting Tool V2" - }, - "description": { "zh": "一个提供多种问候方式的教学工具集。", "en": "A tutorial tool package that demonstrates multiple greeting methods." }, - "category": "Utility", - "tools": [ - { - "name": "greet", - "description": { "zh": "向指定的人发送一个标准的问候。", "en": "Send a standard greeting to a specified person." }, - "parameters": [ - { - "name": "user_name", - "description": { "zh": "要问候的人的名字。", "en": "Name of the person to greet." }, - "type": "string", - "required": true - } - ] - }, - { - "name": "system_inspector", - "description": { "zh": "获取设备摘要信息,并在用户的HOME目录下查找符合特定模式的文件。", "en": "Get device summary information and find files matching a pattern under the user's HOME directory." }, - "parameters": [ - { - "name": "search_pattern", - "description": { "zh": "用于文件搜索的通配符模式,例如:'*.txt' 或 'documents/*.pdf'。", "en": "Glob pattern for file search, e.g. '*.txt' or 'documents/*.pdf'." }, - "type": "string", - "required": true - } - ] - } - ] -}*/ - -// --- 3.2 详解 exports --- -/* - `exports` 是一个空对象,等待你来填充。 - 你必须将你的工具函数赋值给 `exports` 对象的一个属性,属性名必须和 `METADATA` 中定义的工具 `name` 完全一致。 - 只有这样,AI才能把“说明书”和“实际的函数实现”关联起来。 - - `exports.greet = simple_greeter_tool;` - - 上面这行代码的意思是: - “当AI想要调用名为 'greet' 的工具时,请执行 `simple_greeter_tool` 这个函数。” - (我们将在教程的最后进行统一导出,所以暂时注释掉这行) -*/ - - -// ================================================================= -// Part 4: IIFE模式 - 隔离作用域,避免代码冲突 -// ================================================================= -/* - 当项目变大,你可能会定义很多辅助函数、变量。如果都放在全局(文件的顶层),很容易与其他工具或系统库产生命名冲突。 - IIFE (Immediately Invoked Function Expression - 立即调用函数表达式) 是解决这个问题的经典JS模式。 - - 它的写法是 `(function() { ... })();` - - `(function() { ... })` 定义了一个匿名函数。 - - 最后的 `()` 表示“立即执行”这个刚刚定义的匿名函数。 - - 这样做的好处是创建了一个“私有作用域”。在IIFE内部声明的任何变量或函数,在外部都是不可见的,从而避免了污染全局命名空间。 - 我们可以通过 `return` 语句,有选择性地将需要公开的函数暴露出来。 -*/ - -// 使用IIFE模式重构我们的问候工具 -const greeterToolBox = (function () { - // --- 这里是私有作用域 --- - - // `version` 和 `logWithTimestamp` 在IIFE外部无法被访问,是我们的内部实现细节。 - const version = "2.0"; - - function logWithTimestamp(message: string) { - const timestamp = new Date().toLocaleTimeString(); - console.log(`[${timestamp}][v${version}] ${message}`); - } - - // 这是我们真正的业务逻辑实现 - async function performGreeting(params: { user_name: string }) { - try { - logWithTimestamp(`开始执行问候工具,参数: ${JSON.stringify(params)}`); - const { user_name } = params; - if (!user_name) { - throw new Error("参数 'user_name' 缺失。"); - } - const greeting = `你好, ${user_name}! (来自IIFE模式 v${version})`; - complete({ success: true, data: greeting }); - } catch (error) { - logWithTimestamp(`问候工具失败: ${error.message}`); - complete({ success: false, message: error.message }); - } - } - - // --- 公开接口 --- - // 通过 return 一个对象,我们将 `performGreeting` 函数以 `greet` 的名字暴露出去。 - return { - greet: performGreeting - // 如果有其他工具,可以继续在这里暴露: anotherTool: someInternalFunction - }; - -})(); // 注意这里,函数被立即执行了 - -// 现在,IIFE已经执行完毕,`greeterToolBox` 变量包含了我们返回的对象。 -// 我们可以这样导出它: -// `exports.greet = greeterToolBox.greet;` - - -// ================================================================= -// Part 5: Wrapper模式 - 终极形态,让代码更健壮、更简洁 -// ================================================================= -/* - 观察上面的代码,即使使用了IIFE,`performGreeting` 函数内部仍然有重复的 `try/catch` 和 `complete()` 调用。 - 在软件工程中,重复的代码(boilerplate)是维护的噩梦。 - - 我们可以创建一个 `wrapper` (包装器) 函数来终结这种重复。 - Wrapper 的职责非常清晰: - 1. 接收一个“核心业务逻辑函数”作为输入。 - 2. 在内部建立 `try/catch` 安全网。 - 3. 负责调用核心函数,并等待其结果。 - 4. 无论成功还是失败,都由它来调用 `complete()` 并返回标准格式的结果。 - - 这样,我们编写的工具函数就可以变得非常纯粹:只关心输入、业务处理和输出(通过`return`和`throw`), - 完全不用理会 `try/catch` 和 `complete` 这些模板代码。 -*/ - -// --- 最终的、最推荐的开发模式 --- - -const finalTool = (function () { - - // 1. 定义我们的通用 Wrapper 函数 - // 它接收一个核心逻辑函数和其所需的参数。 - async function wrap(coreFunction: (params: any) => Promise, params: any) { - try { - // 调用核心业务逻辑,并使用 await 等待其完成 - const result = await coreFunction(params); - - // 核心逻辑成功返回,Wrapper负责包装成标准成功对象并调用complete - complete({ - success: true, - message: "工具执行成功。", - data: result // 将核心函数的返回值放入data字段 - }); - - } catch (error: any) { - console.error(`工具 [${coreFunction.name}] 执行时捕获到错误:`, error); - - // 核心逻辑抛出异常,Wrapper负责捕获并包装成标准失败对象 - complete({ - success: false, - message: `错误: ${error.message}`, // 将错误信息放入message字段 - error_stack: error.stack // 附带完整的错误堆栈以供调试 - }); - } - } - - // ================================================= - // 核心业务逻辑区 (Core Logic Functions) - // ================================================= - - // 2a. “问候”工具的核心逻辑 - async function greet_core_logic(params: { user_name: string }): Promise { - if (!params.user_name || params.user_name.trim() === '') { - throw new Error("user_name 不能为空。"); - } - const greeting = `你好, ${params.user_name}! 这是一个采用终极Wrapper模式的工具。`; - return greeting; - } - - // 2b. “系统检查器”工具的核心逻辑 - async function inspect_system_core_logic(params: { search_pattern: string }): Promise { - if (!params.search_pattern) { - throw new Error("search_pattern 不能为空。"); - } - - console.log(`开始系统检查,搜索模式: '${params.search_pattern}'`); - - // 为了让逻辑更清晰,我们一步一步地、顺序地调用工具 - // 1. 首先,获取设备信息 - console.log("正在获取设备信息..."); - const deviceInfo = await Tools.System.getDeviceInfo(); - console.log("设备信息获取成功。"); - - // 2. 然后,查找文件 - console.log(`正在主目录 ('~') 下查找文件,模式: '${params.search_pattern}'`); - const foundFiles = await Tools.Files.find('~', params.search_pattern); - console.log("文件查找成功。"); - - // 组合结果并返回一个结构化的对象 - return { - device: deviceInfo, - files: foundFiles - }; - } - - - // ================================================= - // 导出函数封装区 (Exported Functions) - // ================================================= - - // 3a. 暴露给AI的“问候”函数 - async function greet_exported_function(params: { user_name: string }) { - await wrap(greet_core_logic, params); - } - - // 3b. 暴露给AI的“系统检查器”函数 - async function inspect_system_exported_function(params: { search_pattern: string }) { - await wrap(inspect_system_core_logic, params); - } - - - // 4. 从IIFE中返回所有需要导出的工具 - return { - greet: greet_exported_function, - system_inspector: inspect_system_exported_function - }; - -})(); - - -// ================================================================= -// Part 6: 使用内置工具集 (Using the Built-in `Tools`) -// ================================================================= -/* - 沙箱环境提供了一个强大的全局对象 `Tools`,它是你与外部世界交互的主要入口。 - `Tools` 对象被组织成多个模块,每个模块负责一类特定的功能。 - - 主要的模块包括: - - `Tools.System`: 系统级操作。 - - `getDeviceInfo()`: 获取设备信息(型号、操作系统版本等)。 - - `sleep(ms)`: 让工具暂停指定的毫秒数。 - - `exec(command)`: 执行shell命令。 - - `Tools.Files`: 文件系统操作。 - - `read(path)`: 读取文件内容。 - - `write(path, content)`: 写入内容到文件。 - - `find(directory, pattern)`: 在指定目录中查找匹配模式的文件。 - - `remove(path)`: 删除文件或目录。 - - `Tools.Net`: 网络操作。 - - `fetch(url, options)`: 发起HTTP请求,类似于浏览器中的`fetch`。 - - `Tools.UI`: 与设备UI交互(高级功能)。 - - `getScreenshot()`: 获取当前屏幕截图。 - - `findAndClick(text)`: 查找并点击包含特定文本的UI元素。 - - `Tools.Memory`: 记忆管理功能。 - - `query(query)`: 查询记忆库。 - - `create(title, content, ...)`: 创建新记忆。 - - `update(oldTitle, updates)`: 更新记忆。 - - `deleteMemory(title)`: 删除记忆。 - - `link(sourceTitle, targetTitle, ...)`: 链接记忆。 - - `Tools.FFmpeg`: 音视频处理(高级功能)。 - - 所有的 `Tools` 函数都是【异步】的,意味着你必须使用 `await` 来调用它们, - 并且调用它们的代码必须位于 `async` 函数内部。 - - 在上面的 `Part 5` 中,我们已经为你演示了如何实现一个名为 `system_inspector` 的新工具。 - 它完美地展示了: - 1. 如何在核心逻辑函数中调用 `Tools.System.getDeviceInfo()` 和 `Tools.Files.find()`。 - 2. 如何按顺序地调用多个异步工具,等待每一个操作完成后再进行下一步。 - 3. 如何将这些调用集成到我们健壮的 `Wrapper` 模式中。 - - 请回头仔细阅读 `Part 5` 中 `inspect_system_core_logic` 函数的实现, - 它是你学习使用内置 `Tools` 的最佳起点。 -*/ - - -// ========================================== -// 最终导出 (Final Export) -// ========================================== -// 将我们在最终IIFE模式中返回的函数,挂载到exports上。 -// 这里的 `greet` 和 `system_inspector` 必须和METADATA中定义的工具 `name` 完全一致。 -exports.greet = finalTool.greet; -exports.system_inspector = finalTool.system_inspector; - -/* - 🎓 恭喜! 你已经学完了从零到一的完整开发流程。 - - 现在回顾一下我们构建最终代码形态的旅程: - 1. **基础语法**: 掌握了编写代码的基本工具。 - 2. **基础工具+complete()**: 理解了AI工具的本质和与系统的交互方式。 - 3. **Metadata+exports**: 学会了如何让AI“发现”并“使用”你的工具。 - 4. **IIFE模式**: 学会了如何通过作用域隔离来组织代码,避免命名冲突,使代码更模块化。 - 5. **Wrapper模式**: 学会了如何通过分离“业务逻辑”和“模板代码”来编写更简洁、更健壮、更易于维护的工具代码。 - 6. **内置工具 `Tools`**: 学会了如何利用系统提供的强大工具与文件、网络和操作系统进行交互。 - - 这套“Wrapper + IIFE + Tools”的组合是经过实践检验的最佳实践,强烈建议你在开发自己的工具时采用它。 -*/ \ No newline at end of file diff --git a/scripts/qwen_draw.ts b/scripts/qwen_draw.ts deleted file mode 100644 index de3c7a8..0000000 --- a/scripts/qwen_draw.ts +++ /dev/null @@ -1,451 +0,0 @@ -/* METADATA -{ - "name": "qwen_draw", - - "display_name": { - "zh": "Qwen 绘图", - "en": "Qwen Draw" - }, - "description": { - "zh": "使用阿里云百炼/DashScope 文生图接口(通义万相/通义千问图像)根据提示词画图(异步任务轮询),将图片保存到本地 /sdcard/Download/Operit/plugins/draw/qwen_draw/draws/ 目录,并返回 Markdown 图片提示。", - "en": "Generate images via Alibaba Cloud Model Studio (DashScope) text-to-image API (async task polling), save to /sdcard/Download/Operit/plugins/draw/qwen_draw/draws/, and return a Markdown image reference." - }, - "env": [ - { - "name": "DASHSCOPE_API_KEY", - "description": { - "zh": "DashScope API Key(必填)", - "en": "DashScope API key (required)" - }, - "required": true - }, - { - "name": "DASHSCOPE_API_BASE_URL", - "description": { - "zh": "DashScope API Base URL(可选,不填则默认 https://dashscope.aliyuncs.com;国际站可用 https://dashscope-intl.aliyuncs.com)", - "en": "DashScope API base URL (optional; defaults to https://dashscope.aliyuncs.com; intl: https://dashscope-intl.aliyuncs.com)" - }, - "required": false - }, - { - "name": "QWEN_IMAGE_MODEL", - "description": { - "zh": "默认文生图模型(可选;当 draw_image 未传 model 时使用,例如 qwen-image-plus 或 wan2.2-t2i-flash)", - "en": "Default image model (optional; used when draw_image doesn't pass model), e.g. qwen-image-plus or wan2.2-t2i-flash" - }, - "required": false - } - ], - "category": "Draw", - "tools": [ - { - "name": "draw_image", - "description": { - "zh": "根据提示词调用 DashScope 文生图接口生成图片,保存到本地并返回 Markdown 图片提示。", - "en": "Generate an image via DashScope text-to-image API using a prompt, save it locally, and return a Markdown image reference." - }, - "parameters": [ - { "name": "prompt", "description": { "zh": "绘图提示词(英文或中文皆可)", "en": "Prompt for image generation (Chinese or English)" }, "type": "string", "required": true }, - { "name": "model", "description": { "zh": "模型名称(可选;不传则使用环境变量 QWEN_IMAGE_MODEL,再不行使用默认值)", "en": "Model name (optional; falls back to env QWEN_IMAGE_MODEL, then default)" }, "type": "string", "required": false }, - { "name": "size", "description": { "zh": "输出图像分辨率,如 '1664*928' 或 '1024x1024'(可选)", "en": "Output image resolution, e.g. '1664*928' or '1024x1024' (optional)" }, "type": "string", "required": false }, - { "name": "n", "description": { "zh": "生成图片数量(可选,默认 1)", "en": "Number of images (optional; default 1)" }, "type": "number", "required": false }, - { "name": "negative_prompt", "description": { "zh": "负面提示词(可选)", "en": "Negative prompt (optional)" }, "type": "string", "required": false }, - { "name": "prompt_extend", "description": { "zh": "是否开启 prompt 智能改写(可选,默认 true)", "en": "Enable prompt extension (optional; default true)" }, "type": "boolean", "required": false }, - { "name": "watermark", "description": { "zh": "是否加水印(可选,默认 false)", "en": "Enable watermark (optional; default false)" }, "type": "boolean", "required": false }, - { "name": "file_name", "description": { "zh": "自定义保存到本地的文件名(不含路径和扩展名)", "en": "Custom output file name (without path or extension)" }, "type": "string", "required": false }, - { "name": "api_base_url", "description": { "zh": "DashScope API Base URL(不传则取环境变量 DASHSCOPE_API_BASE_URL 或默认 https://dashscope.aliyuncs.com )", "en": "DashScope API base URL (optional; falls back to env DASHSCOPE_API_BASE_URL or https://dashscope.aliyuncs.com)" }, "type": "string", "required": false }, - { "name": "poll_interval_ms", "description": { "zh": "轮询间隔(毫秒),默认 2000", "en": "Polling interval (milliseconds), default 2000" }, "type": "number", "required": false }, - { "name": "max_wait_time_ms", "description": { "zh": "最长等待时间(毫秒),默认 10 分钟", "en": "Max wait time (milliseconds), default 10 minutes" }, "type": "number", "required": false } - ] - } - ] -}*/ - -/// - -const qwenDraw = (function () { - const HTTP_TIMEOUT_MS = 600000; - const client = OkHttp.newBuilder() - .connectTimeout(HTTP_TIMEOUT_MS) - .readTimeout(HTTP_TIMEOUT_MS) - .writeTimeout(HTTP_TIMEOUT_MS) - .build(); - - const DEFAULT_API_BASE_URL = "https://dashscope.aliyuncs.com"; - const DEFAULT_MODEL = "qwen-image-plus"; - - const DRAW_ROOT_DIR = getPluginConfigDir("draw"); - const STORAGE_DIR = `${DRAW_ROOT_DIR}/qwen_draw`; - const DRAWS_DIR = `${STORAGE_DIR}/draws`; - - const POLL_INTERVAL_MS = 2000; - const MAX_WAIT_TIME_MS = 600000; - - function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null; - } - - function getErrorMessage(error: unknown): string { - if (error instanceof Error) return error.message; - return String(error); - } - - function getErrorStack(error: unknown): string | undefined { - if (error instanceof Error) return error.stack; - return undefined; - } - - function normalizePositiveInt(value: unknown, fallback: number): number { - if (value === undefined || value === null) return fallback; - const n = typeof value === "number" ? value : parseInt(String(value), 10); - if (!Number.isFinite(n) || n <= 0) return fallback; - return Math.floor(n); - } - - function joinUrl(baseUrl: string, path: string): string { - const normalizedBase = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`; - const normalizedPath = path.startsWith("/") ? path.slice(1) : path; - return `${normalizedBase}${normalizedPath}`; - } - - function getApiKey(): string { - const apiKey = getEnv("DASHSCOPE_API_KEY"); - if (!apiKey) { - throw new Error("DASHSCOPE_API_KEY 未配置,请在环境变量中设置 DashScope 的 API Key。"); - } - return apiKey; - } - - function getApiBaseUrl(customBaseUrl?: string | null): string { - const fromParam = (customBaseUrl || "").trim(); - if (fromParam) return fromParam; - - const fromEnv = (getEnv("DASHSCOPE_API_BASE_URL") || "").trim(); - if (fromEnv) return fromEnv; - - return DEFAULT_API_BASE_URL; - } - - function getTextToImageEndpoint(baseUrl: string): string { - const trimmed = baseUrl.trim(); - if (!trimmed) return joinUrl(DEFAULT_API_BASE_URL, "api/v1/services/aigc/text2image/image-synthesis"); - - if (trimmed.includes("/api/v1/services/aigc/text2image/image-synthesis")) return trimmed; - - if (trimmed.endsWith("/api/v1")) return joinUrl(trimmed, "services/aigc/text2image/image-synthesis"); - if (trimmed.endsWith("/api/v1/")) return joinUrl(trimmed, "services/aigc/text2image/image-synthesis"); - - return joinUrl(trimmed, "api/v1/services/aigc/text2image/image-synthesis"); - } - - function getTaskEndpoint(baseUrl: string, taskId: string): string { - const trimmed = baseUrl.trim(); - const safeBase = trimmed || DEFAULT_API_BASE_URL; - if (safeBase.includes("/api/v1/tasks/")) { - const idx = safeBase.indexOf("/api/v1/tasks/"); - return safeBase.substring(0, idx) + `/api/v1/tasks/${taskId}`; - } - if (safeBase.endsWith("/api/v1")) return joinUrl(safeBase, `tasks/${taskId}`); - if (safeBase.endsWith("/api/v1/")) return joinUrl(safeBase, `tasks/${taskId}`); - return joinUrl(safeBase, `api/v1/tasks/${taskId}`); - } - - function sanitizeFileName(name: string): string { - const safe = name.replace(/[\\/:*?"<>|]/g, "_").trim(); - if (!safe) return `qwen_draw_${Date.now()}`; - return safe.substring(0, 80); - } - - function buildFileName(prompt: string, customName?: string | null): string { - if (customName && customName.trim().length > 0) { - return sanitizeFileName(customName); - } - const shortPrompt = prompt.length > 40 ? `${prompt.substring(0, 40)}...` : prompt; - const base = sanitizeFileName(shortPrompt || "image"); - const timestamp = Date.now(); - return `${base}_${timestamp}`; - } - - function normalizeSize(size?: string | null): string | undefined { - const raw = String(size || "").trim(); - if (!raw) return undefined; - - const cleaned = raw.replace(/×/g, "*").replace(/x/gi, "*").replace(/\s+/g, ""); - if (!/^\d+\*\d+$/.test(cleaned)) return undefined; - return cleaned; - } - - function guessExtensionFromUrl(url: string): string { - const match = url.match(/\.(png|jpg|jpeg|webp|gif)(?:\?|#|$)/i); - if (match && match[1]) return match[1].toLowerCase(); - return "png"; - } - - async function ensureDirectories(): Promise { - const dirs = [DRAW_ROOT_DIR, STORAGE_DIR, DRAWS_DIR]; - for (const dir of dirs) { - try { - const result = await Tools.Files.mkdir(dir); - if (!result.successful) { - console.warn(`创建目录失败(可能已存在): ${dir} -> ${result.details}`); - } - } catch (e: unknown) { - console.warn(`创建目录异常: ${dir} -> ${getErrorMessage(e)}`); - } - } - } - - async function createTask(params: { - prompt: string; - model?: string; - size?: string; - n?: number; - negative_prompt?: string; - prompt_extend?: boolean; - watermark?: boolean; - api_base_url?: string; - }): Promise<{ task_id: string; effective_model: string }> { - const apiKey = getApiKey(); - const apiBaseUrl = getApiBaseUrl(params.api_base_url); - const endpoint = getTextToImageEndpoint(apiBaseUrl); - - const modelFromParam = (params.model || "").trim(); - const modelFromEnv = (getEnv("QWEN_IMAGE_MODEL") || "").trim(); - const effectiveModel = modelFromParam || modelFromEnv || DEFAULT_MODEL; - - const body: Record = { - model: effectiveModel, - input: { - prompt: params.prompt - }, - parameters: { - n: typeof params.n === "number" && Number.isFinite(params.n) && params.n > 0 ? Math.floor(params.n) : 1, - prompt_extend: params.prompt_extend === undefined ? true : !!params.prompt_extend, - watermark: params.watermark === undefined ? false : !!params.watermark - } - }; - - const normalizedSize = normalizeSize(params.size); - if (normalizedSize) { - (body.parameters as Record)["size"] = normalizedSize; - } - - const negativePrompt = (params.negative_prompt || "").trim(); - if (negativePrompt) { - (body.parameters as Record)["negative_prompt"] = negativePrompt; - } - - const headers = { - "accept": "application/json", - "content-type": "application/json", - "Authorization": `Bearer ${apiKey}`, - "X-DashScope-Async": "enable" - }; - - const request = client - .newRequest() - .url(endpoint) - .method("POST") - .headers(headers) - .body(JSON.stringify(body), "json"); - - const response = await request.build().execute(); - - if (!response.isSuccessful()) { - throw new Error(`DashScope 文生图创建任务失败: ${response.statusCode} - ${response.content}`); - } - - let parsed: unknown; - try { - parsed = JSON.parse(response.content) as unknown; - } catch (e: unknown) { - throw new Error(`解析 DashScope 创建任务响应失败: ${getErrorMessage(e)}`); - } - - const output = isRecord(parsed) && isRecord(parsed["output"]) ? (parsed["output"] as Record) : null; - const taskId = output && typeof output["task_id"] === "string" ? (output["task_id"] as string) : ""; - if (!taskId) { - throw new Error(`DashScope 创建任务响应中未找到 output.task_id: ${response.content}`); - } - - return { task_id: taskId, effective_model: effectiveModel }; - } - - async function pollTask(params: { - task_id: string; - api_base_url?: string; - poll_interval_ms?: number; - max_wait_time_ms?: number; - }): Promise<{ image_url: string; task_status: string }> { - const apiKey = getApiKey(); - const apiBaseUrl = getApiBaseUrl(params.api_base_url); - - const pollIntervalMs = normalizePositiveInt(params.poll_interval_ms, POLL_INTERVAL_MS); - const maxWaitTimeMs = normalizePositiveInt(params.max_wait_time_ms, MAX_WAIT_TIME_MS); - - const startTime = Date.now(); - let attempts = 0; - - while (Date.now() - startTime < maxWaitTimeMs) { - attempts++; - - const endpoint = getTaskEndpoint(apiBaseUrl, params.task_id); - const headers = { - "accept": "application/json", - "Authorization": `Bearer ${apiKey}` - }; - - const request = client - .newRequest() - .url(endpoint) - .method("GET") - .headers(headers); - - const response = await request.build().execute(); - - if (!response.isSuccessful()) { - throw new Error(`DashScope 查询任务失败: ${response.statusCode} - ${response.content}`); - } - - let parsed: unknown; - try { - parsed = JSON.parse(response.content) as unknown; - } catch (e: unknown) { - throw new Error(`解析 DashScope 查询任务响应失败: ${getErrorMessage(e)}`); - } - - const output = isRecord(parsed) && isRecord(parsed["output"]) ? (parsed["output"] as Record) : null; - const taskStatus = output && typeof output["task_status"] === "string" ? String(output["task_status"]) : ""; - - if (taskStatus === "SUCCEEDED") { - const results = output ? output["results"] : null; - const first = Array.isArray(results) && results.length > 0 ? results[0] : null; - const url = isRecord(first) ? first["url"] : undefined; - if ((typeof url !== "string" && typeof url !== "number") || String(url).trim().length === 0) { - throw new Error(`任务已完成但未找到图片 URL: ${response.content}`); - } - return { image_url: String(url), task_status: taskStatus }; - } - - if (taskStatus === "FAILED") { - throw new Error(`任务失败: ${response.content}`); - } - - if (attempts % 5 === 0) { - console.log(`任务状态: ${taskStatus || "UNKNOWN"},继续等待...`); - } - - await Tools.System.sleep(pollIntervalMs); - } - - throw new Error(`任务超时: 等待超过${Math.ceil(maxWaitTimeMs / 60000)}分钟仍未完成`); - } - - async function draw_image(params: { - prompt: string; - model?: string; - size?: string; - n?: number; - negative_prompt?: string; - prompt_extend?: boolean; - watermark?: boolean; - file_name?: string; - api_base_url?: string; - poll_interval_ms?: number; - max_wait_time_ms?: number; - }) { - if (!params || !params.prompt || params.prompt.trim().length === 0) { - throw new Error("参数 prompt 不能为空。"); - } - - const prompt = params.prompt.trim(); - - await ensureDirectories(); - - const createResult = await createTask({ - prompt, - model: params.model, - size: params.size, - n: params.n, - negative_prompt: params.negative_prompt, - prompt_extend: params.prompt_extend, - watermark: params.watermark, - api_base_url: params.api_base_url - }); - - const pollResult = await pollTask({ - task_id: createResult.task_id, - api_base_url: params.api_base_url, - poll_interval_ms: params.poll_interval_ms, - max_wait_time_ms: params.max_wait_time_ms - }); - - const ext = guessExtensionFromUrl(pollResult.image_url); - const baseName = buildFileName(prompt, params.file_name); - const filePath = `${DRAWS_DIR}/${baseName}.${ext}`; - - const downloadResult = await Tools.Files.download(pollResult.image_url, filePath); - if (!downloadResult.successful) { - throw new Error(`下载图片失败: ${downloadResult.details}`); - } - - const fileUri = `file://${filePath}`; - const markdown = `![AI生成的图片](${fileUri})`; - - const hintLines: string[] = []; - hintLines.push(`图片已生成并保存在本地 ${DRAWS_DIR}。`); - hintLines.push(`本地路径: ${filePath}`); - hintLines.push(""); - hintLines.push("在后续回答中,请直接输出下面这一行 Markdown 来展示这张图片:"); - hintLines.push(""); - hintLines.push(markdown); - - return { - file_path: filePath, - file_uri: fileUri, - markdown, - prompt, - model: createResult.effective_model, - task_id: createResult.task_id, - task_status: pollResult.task_status, - image_url: pollResult.image_url, - hint: hintLines.join("\n") - }; - } - - async function draw_image_wrapper(params: { - prompt: string; - model?: string; - size?: string; - n?: number; - negative_prompt?: string; - prompt_extend?: boolean; - watermark?: boolean; - file_name?: string; - api_base_url?: string; - poll_interval_ms?: number; - max_wait_time_ms?: number; - }) { - try { - const result = await draw_image(params); - complete({ - success: true, - message: `图片生成成功,已保存到 ${DRAWS_DIR},并返回 Markdown 图片提示。`, - data: result - }); - } catch (error: unknown) { - console.error("draw_image 执行失败:", error); - complete({ - success: false, - message: `图片生成失败: ${getErrorMessage(error)}`, - error_stack: getErrorStack(error) - }); - } - } - - return { - draw_image: draw_image_wrapper - }; -})(); - -exports.draw_image = qwenDraw.draw_image; diff --git a/scripts/reader.ts b/scripts/reader.ts deleted file mode 100644 index bb4c712..0000000 --- a/scripts/reader.ts +++ /dev/null @@ -1,794 +0,0 @@ -/* METADATA -{ - name: code_reader - - display_name: { - zh: "代码读取器" - en: "Code Reader" - } - description: { zh: "提供代码匹配抓取功能", en: "Provide code searching and extraction utilities." }, - enabledByDefault: true - - category: "File" - tools: [ - { - name: search_code_in_folder - description: { zh: "在文件夹中搜索匹配的代码", en: "Search for matching code in a folder." } - parameters: [ - { - name: folder_path - description: { zh: "要搜索的文件夹路径", en: "Folder path to search." } - type: string - required: true - }, - { - name: pattern - description: { zh: "要搜索的代码模式(字符串或正则表达式)", en: "Pattern to search for (string or regular expression)." } - type: string - required: true - }, - { - name: file_extensions - description: { zh: "要搜索的文件扩展名列表(如 \".ts,.js\")", en: "File extension list to include (e.g. \".ts,.js\")." } - type: string - required: false - }, - { - name: recursive - description: { zh: "是否递归搜索子文件夹(true/false)", en: "Whether to search subfolders recursively (true/false)." } - type: string - required: false - } - ] - }, - { - name: extract_functions - description: { zh: "提取文件夹中的所有函数定义", en: "Extract all function definitions from a folder." } - parameters: [ - { - name: folder_path - description: { zh: "要搜索的文件夹路径", en: "Folder path to search." } - type: string - required: true - }, - { - name: function_name_pattern - description: { zh: "函数名匹配模式(可选)", en: "Function name matching pattern (optional)." } - type: string - required: false - }, - { - name: file_extensions - description: { zh: "要搜索的文件扩展名列表(如 \".ts,.js\")", en: "File extension list to include (e.g. \".ts,.js\")." } - type: string - required: false - }, - { - name: recursive - description: { zh: "是否递归搜索子文件夹(true/false)", en: "Whether to search subfolders recursively (true/false)." } - type: string - required: false - } - ] - }, - { - name: extract_function_block - description: { zh: "提取特定函数的代码块", en: "Extract the code block for a specific function." } - parameters: [ - { - name: file_path - description: { zh: "要读取的文件路径", en: "File path to read." } - type: string - required: true - }, - { - name: function_name - description: { zh: "要提取的函数名称", en: "Function name to extract." } - type: string - required: true - }, - { - name: include_line_numbers - description: { zh: "是否在结果中包含行号(true/false)", en: "Whether to include line numbers in the result (true/false)." } - type: string - required: false - } - ] - }, - { - name: find_code_blocks - description: { zh: "查找文件中的代码块", en: "Find code blocks in a file." } - parameters: [ - { - name: file_path - description: { zh: "要读取的文件路径", en: "File path to read." } - type: string - required: true - }, - { - name: block_pattern - description: { zh: "代码块匹配模式(如类定义、接口等)", en: "Code block matching pattern (e.g., class definition, interface, etc.)." } - type: string - required: true - }, - { - name: include_line_numbers - description: { zh: "是否在结果中包含行号(true/false)", en: "Whether to include line numbers in the result (true/false)." } - type: string - required: false - } - ] - }, - { - name: read_regex_matches - description: { zh: "读取文件中匹配正则表达式的内容", en: "Read content in a file that matches a regular expression." } - parameters: [ - { - name: file_path - description: { zh: "要读取的文件路径", en: "File path to read." } - type: string - required: true - }, - { - name: regex - description: { zh: "正则表达式字符串", en: "Regular expression string." } - type: string - required: true - }, - { - name: include_line_numbers - description: { zh: "是否在结果中包含行号(true/false)", en: "Whether to include line numbers in the result (true/false)." } - type: string - required: false - } - ] - } - ] -}*/ - -/** - * Writer模块 - 提供代码抓取和分析功能 - */ -const writer = (function () { - - /** - * 参数类型转换函数 - 将输入参数转换为期望的数据类型 - * @param params 输入参数对象 - * @param paramTypes 参数类型定义 - * @returns 转换后的参数对象 - */ - function convertParamTypes(params: Record, paramTypes: Record): Record { - if (!params || !paramTypes) return params; - - const result: Record = {}; - - for (const key in params) { - if (params[key] === undefined || params[key] === undefined) { - result[key] = params[key]; - continue; - } - - const expectedType = paramTypes[key]; - if (!expectedType) { - // 如果没有指定类型,保持原样 - result[key] = params[key]; - continue; - } - - // 获取参数值 - const value = params[key]; - - try { - switch (expectedType.toLowerCase()) { - case 'number': - // 将字符串转换为数字 - if (typeof value === 'string') { - if (value.includes('.')) { - result[key] = parseFloat(value); - } else { - result[key] = parseInt(value, 10); - } - - // 检查转换结果是否为有效数字 - if (isNaN(result[key])) { - throw new Error(`参数 ${key} 无法转换为数字: ${value}`); - } - } else { - result[key] = value; - } - break; - - case 'boolean': - // 将字符串转换为布尔值 - if (typeof value === 'string') { - const lowerValue = value.toLowerCase(); - if (lowerValue === 'true' || lowerValue === '1' || lowerValue === 'yes') { - result[key] = true; - } else if (lowerValue === 'false' || lowerValue === '0' || lowerValue === 'no') { - result[key] = false; - } else { - throw new Error(`参数 ${key} 无法转换为布尔值: ${value}`); - } - } else { - result[key] = value; - } - break; - - case 'array': - // 将字符串转换为数组 - if (typeof value === 'string') { - try { - result[key] = JSON.parse(value); - if (!Array.isArray(result[key])) { - throw new Error('解析结果不是数组'); - } - } catch (e) { - throw new Error(`参数 ${key} 无法转换为数组: ${value}`); - } - } else { - result[key] = value; - } - break; - - case 'object': - // 将字符串转换为对象 - if (typeof value === 'string') { - try { - result[key] = JSON.parse(value); - if (Array.isArray(result[key]) || typeof result[key] !== 'object') { - throw new Error('解析结果不是对象'); - } - } catch (e) { - throw new Error(`参数 ${key} 无法转换为对象: ${value}`); - } - } else { - result[key] = value; - } - break; - - default: - // 其他类型或未指定类型,保持原样 - result[key] = value; - } - } catch (error) { - console.error(`参数类型转换错误: ${error.message}`); - // 转换失败时保留原始值 - result[key] = value; - } - } - - return result; - } - - /** - * 包装函数 - 统一处理所有Writer函数的返回结果 - * @param func 原始函数 - * @param params 函数参数 - * @param successMessage 成功消息 - * @param failMessage 失败消息 - */ - async function writer_wrap( - func: (params: any) => Promise, - params: any, - successMessage: string, - failMessage: string - ): Promise { - try { - console.log(`开始执行函数: ${func.name || '匿名函数'}`); - console.log(`参数:`, JSON.stringify(params, undefined, 2)); - - // 执行原始函数 - const result = await func(params); - - console.log(`函数 ${func.name || '匿名函数'} 执行结果:`, JSON.stringify(result, undefined, 2)); - - // 如果原始函数已经调用了complete,就不需要再次调用 - if (result === undefined) return; - - // 根据结果类型处理 - if (typeof result === "boolean") { - // 布尔类型结果 - complete({ - success: result, - message: result ? successMessage : failMessage - }); - } else { - // 数据类型结果 - complete({ - success: true, - message: successMessage, - data: result - }); - } - } catch (error) { - // 详细记录错误信息 - console.error(`函数 ${func.name || '匿名函数'} 执行失败!`); - console.error(`错误信息: ${error.message}`); - console.error(`错误堆栈: ${error.stack}`); - - // 处理错误 - complete({ - success: false, - message: `${failMessage}: ${error.message}`, - error_stack: error.stack - }); - } - } - - /** - * 递归读取目录中的所有文件 - * @param dir 目录路径 - * @param fileList 文件列表累加器 - * @param extensions 文件扩展名过滤列表 - * @returns 文件路径列表 - */ - async function readFilesRecursively(dir: string, fileList: string[] = [], extensions?: string[]): Promise { - try { - // 使用Tools.Files.list替代fs.readdirSync - const listResult = await Tools.Files.list(dir); - const entries = listResult.entries || []; - - // 遍历目录条目 - for (const entry of entries) { - const filePath = `${dir}/${entry.name}`; - - if (entry.isDirectory) { - // 递归处理子目录 - await readFilesRecursively(filePath, fileList, extensions); - } else { - // 过滤文件扩展名 - if (!extensions || extensions.length === 0 || - extensions.some(ext => entry.name.toLowerCase().endsWith(ext.toLowerCase()))) { - fileList.push(filePath); - } - } - } - } catch (error) { - console.error(`读取目录失败: ${error.message}`); - } - - return fileList; - } - - /** - * 在文件夹中搜索匹配的代码 - * @param params 搜索参数 - * @returns 搜索结果 - */ - async function search_code_in_folder(params: any): Promise { - const paramTypes = { - recursive: 'boolean', - }; - - params = convertParamTypes(params, paramTypes); - - const { - folder_path, - pattern, - file_extensions = '.js,.ts,.jsx,.tsx,.java,.cs,.py', - recursive = true - } = params; - - // 验证必需参数 - if (!folder_path) { - throw new Error('文件夹路径不能为空'); - } - if (!pattern) { - throw new Error('搜索模式不能为空'); - } - - try { - const patternRegex = new RegExp(pattern, 'gm'); - const extensions = file_extensions.split(','); - let files: string[]; - - // 根据recursive参数决定是否递归搜索 - if (recursive) { - files = await readFilesRecursively(folder_path, [], extensions); - } else { - // 使用Tools.Files.list替代fs.readdirSync - const listResult = await Tools.Files.list(folder_path); - const entries = listResult.entries || []; - - files = entries - .filter(entry => - !entry.isDirectory && - extensions.some(ext => entry.name.toLowerCase().endsWith(ext.toLowerCase())) - ) - .map(entry => `${folder_path}/${entry.name}`); - } - - // 搜索结果 - const results: any[] = []; - - for (const file of files) { - try { - // 使用Tools.Files.read替代fs.readFileSync - const fileResult = await Tools.Files.read(file); - const content = fileResult.content || ''; - const matches: any[] = []; - let match; - - // 使用正则表达式查找所有匹配项 - patternRegex.lastIndex = 0; // 重置正则表达式索引 - while ((match = patternRegex.exec(content)) !== undefined) { - const lineNumber = content.substring(0, match.index).split('\n').length; - const lineContent = content.split('\n')[lineNumber - 1]; - - matches.push({ - match: match[0], - line: lineNumber, - content: lineContent - }); - } - - if (matches.length > 0) { - results.push({ - file: file, - matches: matches - }); - } - } catch (error) { - console.error(`处理文件 ${file} 时出错: ${error.message}`); - } - } - - return results; - } catch (error) { - throw new Error(`搜索代码时出错: ${error.message}`); - } - } - - /** - * 提取文件夹中的所有函数定义 - * @param params 提取参数 - * @returns 提取结果 - */ - async function extract_functions(params: any): Promise { - const paramTypes = { - recursive: 'boolean', - }; - - params = convertParamTypes(params, paramTypes); - - const { - folder_path, - function_name_pattern = '.*', - file_extensions = '.js,.ts,.jsx,.tsx', - recursive = true - } = params; - - // 验证必需参数 - if (!folder_path) { - throw new Error('文件夹路径不能为空'); - } - - try { - // 函数定义的正则表达式模式 - // 匹配函数声明、函数表达式、箭头函数、类方法等 - const functionPatterns = [ - // 函数声明: function name() {} - `function\\s+(${function_name_pattern})\\s*\\([^)]*\\)\\s*\\{`, - // 函数表达式: const name = function() {} - `(?:const|let|var)\\s+(${function_name_pattern})\\s*=\\s*function\\s*\\([^)]*\\)\\s*\\{`, - // 箭头函数: const name = () => {} - `(?:const|let|var)\\s+(${function_name_pattern})\\s*=\\s*\\([^)]*\\)\\s*=>\\s*\\{`, - // 类方法: name() {} - `(?:async\\s+)?(${function_name_pattern})\\s*\\([^)]*\\)\\s*\\{` - ]; - - // 合并所有模式 - const combinedPattern = functionPatterns.join('|'); - - // 使用通用代码搜索功能查找所有函数 - return await search_code_in_folder({ - folder_path, - pattern: combinedPattern, - file_extensions, - recursive - }); - - } catch (error) { - throw new Error(`提取函数时出错: ${error.message}`); - } - } - - /** - * 提取特定函数的代码块 - * @param params 提取参数 - * @returns 提取结果 - */ - async function extract_function_block(params: any): Promise { - const paramTypes = { - include_line_numbers: 'boolean', - }; - - params = convertParamTypes(params, paramTypes); - - const { - file_path, - function_name, - include_line_numbers = false - } = params; - - // 验证必需参数 - if (!file_path) { - throw new Error('文件路径不能为空'); - } - if (!function_name) { - throw new Error('函数名称不能为空'); - } - - try { - // 使用Tools.Files.read替代fs.readFileSync - const fileResult = await Tools.Files.read(file_path); - const content = fileResult.content || ''; - const lines = content.split('\n'); - - // 函数定义的正则表达式模式 - const functionPatterns = [ - // 函数声明: function name() {} - `function\\s+${function_name}\\s*\\([^)]*\\)\\s*\\{`, - // 函数表达式: const name = function() {} - `(?:const|let|var)\\s+${function_name}\\s*=\\s*function\\s*\\([^)]*\\)\\s*\\{`, - // 箭头函数: const name = () => {} - `(?:const|let|var)\\s+${function_name}\\s*=\\s*\\([^)]*\\)\\s*=>\\s*\\{`, - // 类方法: name() {} - `(?:async\\s+)?${function_name}\\s*\\([^)]*\\)\\s*\\{` - ]; - - // 合并所有模式 - const combinedPattern = functionPatterns.join('|'); - const functionRegex = new RegExp(combinedPattern); - - // 查找函数定义行 - let startLine = -1; - for (let i = 0; i < lines.length; i++) { - if (functionRegex.test(lines[i])) { - startLine = i; - break; - } - } - - if (startLine === -1) { - return { error: `未找到函数: ${function_name}` }; - } - - // 追踪括号匹配,找到函数结束位置 - let bracesCount = 0; - let endLine = startLine; - - // 检查函数定义那一行的花括号 - const openingBraces = (lines[startLine].match(/{/g) || []).length; - const closingBraces = (lines[startLine].match(/}/g) || []).length; - bracesCount = openingBraces - closingBraces; - - // 继续向下查找匹配的闭合括号 - for (let i = startLine + 1; i < lines.length; i++) { - const openBraces = (lines[i].match(/{/g) || []).length; - const closeBraces = (lines[i].match(/}/g) || []).length; - - bracesCount += openBraces; - bracesCount -= closeBraces; - - // 当括号计数归零,找到函数结束位置 - if (bracesCount === 0) { - endLine = i; - break; - } - } - - // 提取函数代码块 - const functionBlock = lines.slice(startLine, endLine + 1).join('\n'); - - const result: any = { - function_name, - code: functionBlock, - }; - - if (include_line_numbers) { - result.start_line = startLine + 1; // 转换为1-based索引 - result.end_line = endLine + 1; - } - - return result; - - } catch (error) { - throw new Error(`提取函数代码块时出错: ${error.message}`); - } - } - - /** - * 查找文件中的代码块 - * @param params 查找参数 - * @returns 查找结果 - */ - async function find_code_blocks(params: any): Promise { - const paramTypes = { - include_line_numbers: 'boolean', - }; - - params = convertParamTypes(params, paramTypes); - - const { - file_path, - block_pattern, - include_line_numbers = false - } = params; - - // 验证必需参数 - if (!file_path) { - throw new Error('文件路径不能为空'); - } - if (!block_pattern) { - throw new Error('代码块匹配模式不能为空'); - } - - try { - // 使用Tools.Files.read替代fs.readFileSync - const fileResult = await Tools.Files.read(file_path); - const content = fileResult.content || ''; - const blockRegex = new RegExp(block_pattern, 'gm'); - - const matches: any[] = []; - let match; - - // 使用正则表达式查找所有匹配项 - while ((match = blockRegex.exec(content)) !== undefined) { - const matchText = match[0]; - const startIndex = match.index; - - // 查找代码块的开始和结束位置 - const lines = content.substring(0, startIndex).split('\n'); - const startLine = lines.length; - - let endIndex = startIndex + matchText.length; - let bracesCount = 0; - - // 如果匹配到的是代码块开始,需要找到对应的结束 - if (matchText.includes('{')) { - // 计算初始左括号数量 - const initialOpenBraces = (matchText.match(/{/g) || []).length; - const initialCloseBraces = (matchText.match(/}/g) || []).length; - bracesCount = initialOpenBraces - initialCloseBraces; - - // 如果括号不平衡,继续向后查找 - if (bracesCount > 0) { - const remainingContent = content.substring(endIndex); - let currentPos = 0; - - // 逐字符扫描,跟踪括号嵌套 - for (let i = 0; i < remainingContent.length; i++) { - const char = remainingContent[i]; - - if (char === '{') { - bracesCount++; - } else if (char === '}') { - bracesCount--; - - // 找到匹配的闭合括号 - if (bracesCount === 0) { - currentPos = i + 1; // +1 包含闭合括号 - break; - } - } - } - - // 更新块的结束位置 - endIndex += currentPos; - } - } - - // 提取完整的代码块 - const codeBlock = content.substring(startIndex, endIndex); - - // 确定结束行号 - const blockLines = codeBlock.split('\n'); - const endLine = startLine + blockLines.length - 1; - - const result: any = { - block: codeBlock, - }; - - if (include_line_numbers) { - result.start_line = startLine; - result.end_line = endLine; - } - - matches.push(result); - } - - return { - file: file_path, - blocks: matches - }; - - } catch (error) { - throw new Error(`查找代码块时出错: ${error.message}`); - } - } - - /** - * 读取文件中匹配正则表达式的内容 - * @param params 读取参数 - * @returns 读取结果 - */ - async function read_regex_matches(params: any): Promise { - const paramTypes = { - include_line_numbers: 'boolean', - }; - - params = convertParamTypes(params, paramTypes); - - const { - file_path, - regex, - include_line_numbers = false - } = params; - - // 验证必需参数 - if (!file_path) { - throw new Error('文件路径不能为空'); - } - if (!regex) { - throw new Error('正则表达式不能为空'); - } - - try { - // 使用Tools.Files.read替代fs.readFileSync - const fileResult = await Tools.Files.read(file_path); - const content = fileResult.content || ''; - const lines = content.split('\n'); - const pattern = new RegExp(regex, 'gm'); - - const matches: any[] = []; - let match; - - // 使用正则表达式查找所有匹配项 - while ((match = pattern.exec(content)) !== undefined) { - const lineNumber = content.substring(0, match.index).split('\n').length; - const lineContent = content.split('\n')[lineNumber - 1]; - - const result: any = { - match: match[0], - content: lineContent, - }; - - if (include_line_numbers) { - result.line = lineNumber; - } - - // 添加捕获组 - if (match.length > 1) { - result.groups = match.slice(1); - } - - matches.push(result); - } - - return { - file: file_path, - matches: matches - }; - - } catch (error) { - throw new Error(`匹配正则表达式时出错: ${error.message}`); - } - } - - // 返回模块公共方法 - return { - search_code_in_folder: (params: any) => writer_wrap(search_code_in_folder, params, '代码搜索完成', '代码搜索失败'), - extract_functions: (params: any) => writer_wrap(extract_functions, params, '函数提取完成', '函数提取失败'), - extract_function_block: (params: any) => writer_wrap(extract_function_block, params, '函数代码块提取完成', '函数代码块提取失败'), - find_code_blocks: (params: any) => writer_wrap(find_code_blocks, params, '代码块查找完成', '代码块查找失败'), - read_regex_matches: (params: any) => writer_wrap(read_regex_matches, params, '正则表达式匹配完成', '正则表达式匹配失败') - }; -})(); - -// 导出所有函数 -exports.search_code_in_folder = writer.search_code_in_folder; -exports.extract_functions = writer.extract_functions; -exports.extract_function_block = writer.extract_function_block; -exports.find_code_blocks = writer.find_code_blocks; -exports.read_regex_matches = writer.read_regex_matches; diff --git a/scripts/siliconflow_draw.ts b/scripts/siliconflow_draw.ts deleted file mode 100644 index 0d2eeb2..0000000 --- a/scripts/siliconflow_draw.ts +++ /dev/null @@ -1,668 +0,0 @@ -/* METADATA -{ - "name": "siliconflow_draw", - "display_name": { - "zh": "硅基流动绘图", - "en": "SiliconFlow Draw" - }, - "description": { - "zh": "使用 SiliconFlow 官方图像与视频接口生成图片和视频。图片走 /v1/images/generations,视频走 /v1/video/submit + /v1/video/status;生成结果会立即下载到本地,避免官方临时链接过期。", - "en": "Generate images and videos with SiliconFlow official image and video APIs. Images use /v1/images/generations; videos use /v1/video/submit + /v1/video/status. Generated assets are downloaded locally immediately before temporary URLs expire." - }, - "category": "Draw", - "env": [ - { - "name": "SILICONFLOW_API_KEY", - "description": { - "zh": "SiliconFlow API Key(必填)", - "en": "SiliconFlow API key (required)" - }, - "required": true - }, - { - "name": "SILICONFLOW_API_BASE_URL", - "description": { - "zh": "SiliconFlow API Base URL(可选,不填默认 https://api.siliconflow.cn )", - "en": "SiliconFlow API base URL (optional; defaults to https://api.siliconflow.cn)" - }, - "required": false - }, - { - "name": "SILICONFLOW_IMAGE_MODEL", - "description": { - "zh": "默认生图模型(可选;例如 Kwai-Kolors/Kolors、Qwen/Qwen-Image)", - "en": "Default image model (optional), e.g. Kwai-Kolors/Kolors or Qwen/Qwen-Image" - }, - "required": false - }, - { - "name": "SILICONFLOW_VIDEO_MODEL", - "description": { - "zh": "默认生视频模型(可选;例如 Wan-AI/Wan2.2-T2V-A14B 或 Wan-AI/Wan2.2-I2V-A14B)", - "en": "Default video model (optional), e.g. Wan-AI/Wan2.2-T2V-A14B or Wan-AI/Wan2.2-I2V-A14B" - }, - "required": false - } - ], - "tools": [ - { - "name": "draw_image", - "description": { - "zh": "调用 SiliconFlow 官方图片生成接口生图,下载到本地并返回 Markdown 图片提示。", - "en": "Generate images with SiliconFlow official image API, download locally, and return Markdown image hints." - }, - "parameters": [ - { "name": "prompt", "description": { "zh": "生图提示词", "en": "Image prompt" }, "type": "string", "required": true }, - { "name": "model", "description": { "zh": "模型名;不传则优先取 SILICONFLOW_IMAGE_MODEL,再取默认值 Kwai-Kolors/Kolors", "en": "Model name; falls back to SILICONFLOW_IMAGE_MODEL, then Kwai-Kolors/Kolors" }, "type": "string", "required": false }, - { "name": "image_size", "description": { "zh": "图片尺寸,例如 1024x1024;未传时默认 1024x1024", "en": "Image size, e.g. 1024x1024; defaults to 1024x1024" }, "type": "string", "required": false }, - { "name": "negative_prompt", "description": { "zh": "负面提示词(可选)", "en": "Negative prompt (optional)" }, "type": "string", "required": false }, - { "name": "batch_size", "description": { "zh": "一次生成多少张图(可选)", "en": "Number of images to generate (optional)" }, "type": "number", "required": false }, - { "name": "num_inference_steps", "description": { "zh": "推理步数(可选)", "en": "Inference steps (optional)" }, "type": "number", "required": false }, - { "name": "guidance_scale", "description": { "zh": "提示词引导强度(可选)", "en": "Guidance scale (optional)" }, "type": "number", "required": false }, - { "name": "seed", "description": { "zh": "随机种子(可选)", "en": "Seed (optional)" }, "type": "number", "required": false }, - { "name": "file_name", "description": { "zh": "本地保存文件名(不含扩展名)", "en": "Local output filename without extension" }, "type": "string", "required": false }, - { "name": "api_base_url", "description": { "zh": "自定义 API Base URL(可选)", "en": "Custom API base URL (optional)" }, "type": "string", "required": false } - ] - }, - { - "name": "draw_video", - "description": { - "zh": "调用 SiliconFlow 官方视频生成接口生视频,轮询状态,下载到本地并返回本地视频链接提示。", - "en": "Generate videos with SiliconFlow official video API, poll status, download locally, and return local video link hints." - }, - "parameters": [ - { "name": "prompt", "description": { "zh": "生视频提示词", "en": "Video prompt" }, "type": "string", "required": true }, - { "name": "model", "description": { "zh": "模型名;不传则优先取 SILICONFLOW_VIDEO_MODEL,若传了 image_url/image_path 默认走 Wan-AI/Wan2.2-I2V-A14B,否则默认 Wan-AI/Wan2.2-T2V-A14B", "en": "Model name; falls back to SILICONFLOW_VIDEO_MODEL. If image_url/image_path is provided, defaults to Wan-AI/Wan2.2-I2V-A14B; otherwise Wan-AI/Wan2.2-T2V-A14B" }, "type": "string", "required": false }, - { "name": "image_size", "description": { "zh": "视频尺寸,可选 1280x720、720x1280、960x960;默认 1280x720", "en": "Video size: 1280x720, 720x1280, or 960x960; defaults to 1280x720" }, "type": "string", "required": false }, - { "name": "negative_prompt", "description": { "zh": "负面提示词(可选)", "en": "Negative prompt (optional)" }, "type": "string", "required": false }, - { "name": "image_url", "description": { "zh": "图生视频输入图 URL(可选)", "en": "Input image URL for image-to-video (optional)" }, "type": "string", "required": false }, - { "name": "image_path", "description": { "zh": "图生视频输入图本地路径(可选,会转成 data URL 上传)", "en": "Local input image path for image-to-video (optional; converted to data URL)" }, "type": "string", "required": false }, - { "name": "seed", "description": { "zh": "随机种子(可选)", "en": "Seed (optional)" }, "type": "number", "required": false }, - { "name": "file_name", "description": { "zh": "本地保存文件名(不含扩展名)", "en": "Local output filename without extension" }, "type": "string", "required": false }, - { "name": "api_base_url", "description": { "zh": "自定义 API Base URL(可选)", "en": "Custom API base URL (optional)" }, "type": "string", "required": false }, - { "name": "poll_interval_ms", "description": { "zh": "轮询间隔毫秒数,默认 5000", "en": "Polling interval in ms, default 5000" }, "type": "number", "required": false }, - { "name": "max_wait_time_ms", "description": { "zh": "最大等待毫秒数,默认 600000", "en": "Maximum wait time in ms, default 600000" }, "type": "number", "required": false } - ] - } - ] -}*/ -/// - -const siliconflowDraw = (function () { - const HTTP_TIMEOUT_MS = 600000; - const client = OkHttp.newBuilder() - .connectTimeout(HTTP_TIMEOUT_MS) - .readTimeout(HTTP_TIMEOUT_MS) - .writeTimeout(HTTP_TIMEOUT_MS) - .build(); - - const DEFAULT_API_BASE_URL = "https://api.siliconflow.cn"; - const DEFAULT_IMAGE_MODEL = "Kwai-Kolors/Kolors"; - const DEFAULT_IMAGE_SIZE = "1024x1024"; - const DEFAULT_VIDEO_TEXT_MODEL = "Wan-AI/Wan2.2-T2V-A14B"; - const DEFAULT_VIDEO_IMAGE_MODEL = "Wan-AI/Wan2.2-I2V-A14B"; - const DEFAULT_VIDEO_SIZE = "1280x720"; - const DEFAULT_POLL_INTERVAL_MS = 5000; - const DEFAULT_MAX_WAIT_TIME_MS = 600000; - const VIDEO_SIZE_OPTIONS = ["1280x720", "720x1280", "960x960"]; - - const DRAW_ROOT_DIR = getPluginConfigDir("draw"); - const STORAGE_DIR = `${DRAW_ROOT_DIR}/siliconflow_draw`; - const DRAWS_DIR = `${STORAGE_DIR}/draws`; - const VIDEOS_DIR = `${STORAGE_DIR}/videos`; - - type JsonMap = Record; - - type ImageParams = { - prompt: string; - model?: string; - image_size?: string; - negative_prompt?: string; - batch_size?: number; - num_inference_steps?: number; - guidance_scale?: number; - seed?: number; - file_name?: string; - api_base_url?: string; - }; - - type VideoParams = { - prompt: string; - model?: string; - image_size?: string; - negative_prompt?: string; - image_url?: string; - image_path?: string; - seed?: number; - file_name?: string; - api_base_url?: string; - poll_interval_ms?: number; - max_wait_time_ms?: number; - }; - - type ImageApiResult = { - effective_model: string; - image_urls: string[]; - seed: any; - }; - - type VideoTaskResult = { - api_base_url: string; - effective_model: string; - request_id: string; - seed: any; - }; - - type VideoStatusResult = { - status: string; - reason: string; - video_url: string; - seed: any; - }; - - type ImageFileItem = { - file_path: string; - file_uri: string; - markdown: string; - }; - - function getApiKey() { - const apiKey = getEnv("SILICONFLOW_API_KEY"); - if (!apiKey) { - throw new Error("SILICONFLOW_API_KEY 未配置,请先在环境变量中设置硅基流动 API Key。"); - } - return apiKey; - } - - function getApiBaseUrl(customBaseUrl) { - const fromParam = String(customBaseUrl || "").trim(); - if (fromParam) return fromParam; - - const fromEnv = String(getEnv("SILICONFLOW_API_BASE_URL") || "").trim(); - if (fromEnv) return fromEnv; - - return DEFAULT_API_BASE_URL; - } - - function joinUrl(baseUrl, path) { - const normalizedBase = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`; - const normalizedPath = path.startsWith("/") ? path.slice(1) : path; - return `${normalizedBase}${normalizedPath}`; - } - - function getImageEndpoint(baseUrl) { - const trimmed = String(baseUrl || "").trim(); - if (!trimmed) return joinUrl(DEFAULT_API_BASE_URL, "v1/images/generations"); - if (trimmed.includes("/v1/images/generations")) return trimmed; - if (trimmed.endsWith("/v1")) return joinUrl(trimmed, "images/generations"); - if (trimmed.endsWith("/v1/")) return joinUrl(trimmed, "images/generations"); - return joinUrl(trimmed, "v1/images/generations"); - } - - function getVideoSubmitEndpoint(baseUrl) { - const trimmed = String(baseUrl || "").trim(); - if (!trimmed) return joinUrl(DEFAULT_API_BASE_URL, "v1/video/submit"); - if (trimmed.includes("/v1/video/submit")) return trimmed; - if (trimmed.endsWith("/v1")) return joinUrl(trimmed, "video/submit"); - if (trimmed.endsWith("/v1/")) return joinUrl(trimmed, "video/submit"); - return joinUrl(trimmed, "v1/video/submit"); - } - - function getVideoStatusEndpoint(baseUrl) { - const trimmed = String(baseUrl || "").trim(); - if (!trimmed) return joinUrl(DEFAULT_API_BASE_URL, "v1/video/status"); - if (trimmed.includes("/v1/video/status")) return trimmed; - if (trimmed.endsWith("/v1")) return joinUrl(trimmed, "video/status"); - if (trimmed.endsWith("/v1/")) return joinUrl(trimmed, "video/status"); - return joinUrl(trimmed, "v1/video/status"); - } - - function sanitizeFileName(name, prefix) { - const safe = String(name || "").replace(/[\\/:*?"<>|]/g, "_").trim(); - if (!safe) return `${prefix}_${Date.now()}`; - return safe.substring(0, 80); - } - - function buildFileBaseName(prompt, customName, prefix) { - if (customName && String(customName).trim().length > 0) { - return sanitizeFileName(customName, prefix); - } - const rawPrompt = String(prompt || "").trim(); - const shortPrompt = rawPrompt.length > 40 ? `${rawPrompt.substring(0, 40)}...` : rawPrompt; - return `${sanitizeFileName(shortPrompt || prefix, prefix)}_${Date.now()}`; - } - - function getErrorMessage(error) { - if (error instanceof Error) return error.message; - return String(error); - } - - function guessMimeTypeFromPath(filePath) { - const lower = String(filePath || "").toLowerCase(); - if (lower.endsWith(".png")) return "image/png"; - if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg"; - if (lower.endsWith(".webp")) return "image/webp"; - if (lower.endsWith(".gif")) return "image/gif"; - return "image/png"; - } - - function guessExtensionFromUrl(url, fallbackExtension) { - const match = String(url || "").match(/\.([a-z0-9]{2,5})(?:[?#].*)?$/i); - if (match && match[1]) return match[1].toLowerCase(); - return fallbackExtension; - } - - function parsePositiveInteger(value, fieldName) { - if (value === undefined || value === null || value === "") return undefined; - const numberValue = Number(value); - if (!Number.isFinite(numberValue) || numberValue <= 0) { - throw new Error(`${fieldName} 必须是正整数。`); - } - return Math.floor(numberValue); - } - - function parseFiniteNumber(value, fieldName) { - if (value === undefined || value === null || value === "") return undefined; - const numberValue = Number(value); - if (!Number.isFinite(numberValue)) { - throw new Error(`${fieldName} 必须是有效数字。`); - } - return numberValue; - } - - function parseSeed(value) { - if (value === undefined || value === null || value === "") return undefined; - const numberValue = Number(value); - if (!Number.isFinite(numberValue)) { - throw new Error("seed 必须是有效数字。"); - } - return Math.floor(numberValue); - } - - function normalizeImageSize(imageSize, model) { - const trimmedModel = String(model || "").trim().toLowerCase(); - const rawSize = String(imageSize || "").trim(); - const isQwenEditModel = trimmedModel.startsWith("qwen/qwen-image-edit"); - if (isQwenEditModel) { - if (rawSize) { - throw new Error("当前模型为 Qwen 图像编辑模型,官方文档标注不支持 image_size 参数,请去掉 image_size。"); - } - return undefined; - } - if (!rawSize) return DEFAULT_IMAGE_SIZE; - if (!/^\d+x\d+$/i.test(rawSize)) { - throw new Error("image_size 格式必须是 widthxheight,例如 1024x1024。"); - } - return rawSize.toLowerCase(); - } - - function normalizeVideoSize(imageSize) { - const rawSize = String(imageSize || "").trim(); - if (!rawSize) return DEFAULT_VIDEO_SIZE; - if (!VIDEO_SIZE_OPTIONS.includes(rawSize)) { - throw new Error(`image_size 仅支持 ${VIDEO_SIZE_OPTIONS.join("、")}。`); - } - return rawSize; - } - - function isProbablyUrl(value) { - return /^https?:\/\//i.test(String(value || "").trim()); - } - - async function ensureDirectories() { - const dirs = [DRAW_ROOT_DIR, STORAGE_DIR, DRAWS_DIR, VIDEOS_DIR]; - for (const dir of dirs) { - try { - const result = await Tools.Files.mkdir(dir); - if (!result.successful) { - console.warn(`创建目录失败(可能已存在): ${dir} -> ${result.details}`); - } - } catch (error) { - console.warn(`创建目录异常: ${dir} -> ${getErrorMessage(error)}`); - } - } - } - - async function readLocalImageAsDataUrl(filePath) { - const trimmedPath = String(filePath || "").trim(); - if (!trimmedPath) { - throw new Error("image_path 不能为空。"); - } - const existsResult = await Tools.Files.exists(trimmedPath); - if (!existsResult.exists) { - throw new Error(`本地图片不存在: ${trimmedPath}`); - } - const binaryResult = await Tools.Files.readBinary(trimmedPath); - const base64Content = binaryResult && binaryResult.contentBase64 ? String(binaryResult.contentBase64).trim() : ""; - if (!base64Content) { - throw new Error(`读取本地图片失败: ${trimmedPath}`); - } - return `data:${guessMimeTypeFromPath(trimmedPath)};base64,${base64Content}`; - } - - async function resolveVideoImageInput(imageUrl, imagePath) { - const trimmedUrl = String(imageUrl || "").trim(); - const trimmedPath = String(imagePath || "").trim(); - if (trimmedUrl && trimmedPath) { - throw new Error("image_url 和 image_path 只能二选一,请不要同时传。"); - } - if (trimmedUrl) { - if (!isProbablyUrl(trimmedUrl)) { - throw new Error("image_url 必须是 http 或 https 链接。"); - } - return trimmedUrl; - } - if (trimmedPath) { - return await readLocalImageAsDataUrl(trimmedPath); - } - return undefined; - } - - async function executeJsonRequest(url: string, body: JsonMap): Promise { - const apiKey = getApiKey(); - const request = client - .newRequest() - .url(url) - .method("POST") - .headers({ - "accept": "application/json", - "content-type": "application/json", - "Authorization": `Bearer ${apiKey}` - }) - .body(JSON.stringify(body), "json"); - - const response = await request.build().execute(); - if (!response.isSuccessful()) { - throw new Error(`HTTP ${response.statusCode}: ${response.content}`); - } - try { - return JSON.parse(response.content); - } catch (error) { - throw new Error(`解析响应 JSON 失败: ${getErrorMessage(error)}`); - } - } - - async function callImageApi(params: ImageParams): Promise { - const apiBaseUrl = getApiBaseUrl(params.api_base_url); - const endpoint = getImageEndpoint(apiBaseUrl); - - const modelFromParam = String(params.model || "").trim(); - const modelFromEnv = String(getEnv("SILICONFLOW_IMAGE_MODEL") || "").trim(); - const effectiveModel = modelFromParam || modelFromEnv || DEFAULT_IMAGE_MODEL; - - const body: JsonMap = { - model: effectiveModel, - prompt: String(params.prompt || "").trim() - }; - - const imageSize = normalizeImageSize(params.image_size, effectiveModel); - if (imageSize) body.image_size = imageSize; - - const negativePrompt = String(params.negative_prompt || "").trim(); - if (negativePrompt) body.negative_prompt = negativePrompt; - - const batchSize = parsePositiveInteger(params.batch_size, "batch_size"); - if (batchSize !== undefined) body.batch_size = batchSize; - - const numInferenceSteps = parsePositiveInteger(params.num_inference_steps, "num_inference_steps"); - if (numInferenceSteps !== undefined) body.num_inference_steps = numInferenceSteps; - - const guidanceScale = parseFiniteNumber(params.guidance_scale, "guidance_scale"); - if (guidanceScale !== undefined) body.guidance_scale = guidanceScale; - - const seed = parseSeed(params.seed); - if (seed !== undefined) body.seed = seed; - - const parsed: any = await executeJsonRequest(endpoint, body); - const images = Array.isArray(parsed && parsed.images) ? parsed.images : []; - const imageUrls = images - .map(item => item && item.url ? String(item.url).trim() : "") - .filter(item => item.length > 0); - - if (imageUrls.length === 0) { - throw new Error("图片接口返回中未找到 images[].url,请检查模型与参数是否匹配。"); - } - - return { - effective_model: effectiveModel, - image_urls: imageUrls, - seed: parsed && parsed.seed !== undefined ? parsed.seed : (seed !== undefined ? seed : null) - }; - } - - async function createVideoTask(params: VideoParams, imageInput?: string): Promise { - const apiBaseUrl = getApiBaseUrl(params.api_base_url); - const endpoint = getVideoSubmitEndpoint(apiBaseUrl); - - const modelFromParam = String(params.model || "").trim(); - const modelFromEnv = String(getEnv("SILICONFLOW_VIDEO_MODEL") || "").trim(); - const defaultVideoModel = imageInput ? DEFAULT_VIDEO_IMAGE_MODEL : DEFAULT_VIDEO_TEXT_MODEL; - const effectiveModel = modelFromParam || modelFromEnv || defaultVideoModel; - - if (effectiveModel === DEFAULT_VIDEO_IMAGE_MODEL && !imageInput) { - throw new Error(`当前模型 ${DEFAULT_VIDEO_IMAGE_MODEL} 为图生视频模型,必须传 image_url 或 image_path。`); - } - - const body: JsonMap = { - model: effectiveModel, - prompt: String(params.prompt || "").trim(), - image_size: normalizeVideoSize(params.image_size) - }; - - const negativePrompt = String(params.negative_prompt || "").trim(); - if (negativePrompt) body.negative_prompt = negativePrompt; - - if (imageInput) body.image = imageInput; - - const seed = parseSeed(params.seed); - if (seed !== undefined) body.seed = seed; - - const parsed: any = await executeJsonRequest(endpoint, body); - const requestId = parsed && parsed.requestId ? String(parsed.requestId).trim() : ""; - if (!requestId) { - throw new Error("视频接口返回中未找到 requestId。"); - } - - return { - api_base_url: apiBaseUrl, - effective_model: effectiveModel, - request_id: requestId, - seed: seed !== undefined ? seed : null - }; - } - - async function queryVideoStatus(apiBaseUrl: string, requestId: string): Promise { - const endpoint = getVideoStatusEndpoint(apiBaseUrl); - const parsed: any = await executeJsonRequest(endpoint, { requestId }); - const status = parsed && parsed.status ? String(parsed.status).trim() : ""; - const reason = parsed && parsed.reason ? String(parsed.reason).trim() : ""; - const videos: any[] = parsed && parsed.results && Array.isArray(parsed.results.videos) ? parsed.results.videos : []; - const videoUrl = videos.length > 0 && videos[0] && videos[0].url ? String(videos[0].url).trim() : ""; - const seed = parsed && parsed.results && parsed.results.seed !== undefined ? parsed.results.seed : null; - return { - status, - reason, - video_url: videoUrl, - seed - }; - } - - function normalizeVideoStatus(status) { - return String(status || "").trim().toLowerCase(); - } - - async function draw_image(params: ImageParams) { - const prompt = String(params && params.prompt ? params.prompt : "").trim(); - if (!prompt) { - throw new Error("prompt 不能为空。"); - } - - await ensureDirectories(); - - const apiResult = await callImageApi(params); - const baseName = buildFileBaseName(prompt, params.file_name, "siliconflow_image"); - const files: ImageFileItem[] = []; - - for (let index = 0; index < apiResult.image_urls.length; index += 1) { - const imageUrl = apiResult.image_urls[index]; - const extension = guessExtensionFromUrl(imageUrl, "png"); - const suffix = apiResult.image_urls.length > 1 ? `_${index + 1}` : ""; - const filePath = `${DRAWS_DIR}/${baseName}${suffix}.${extension}`; - const downloadResult = await Tools.Files.download(imageUrl, filePath); - if (!downloadResult.successful) { - throw new Error(`下载图片失败: ${downloadResult.details}`); - } - const fileUri = `file://${filePath}`; - files.push({ - file_path: filePath, - file_uri: fileUri, - markdown: `![AI生成的图片${apiResult.image_urls.length > 1 ? ` ${index + 1}` : ""}](${fileUri})` - }); - } - - const hintLines: string[] = []; - hintLines.push(`图片已生成并下载到 ${DRAWS_DIR}。`); - hintLines.push(`共生成 ${files.length} 张。`); - hintLines.push(""); - hintLines.push("后续回答如果需要展示图片,请直接输出下面这些 Markdown:"); - hintLines.push(""); - for (const fileItem of files) { - hintLines.push(fileItem.markdown); - } - - return { - prompt, - model: apiResult.effective_model, - seed: apiResult.seed, - file_path: files[0].file_path, - file_uri: files[0].file_uri, - markdown: files[0].markdown, - files, - hint: hintLines.join("\n") - }; - } - - async function draw_video(params: VideoParams) { - const prompt = String(params && params.prompt ? params.prompt : "").trim(); - if (!prompt) { - throw new Error("prompt 不能为空。"); - } - - await ensureDirectories(); - - const imageInput = await resolveVideoImageInput(params.image_url, params.image_path); - const createdTask = await createVideoTask(params, imageInput); - const pollIntervalMs = parsePositiveInteger(params.poll_interval_ms, "poll_interval_ms") || DEFAULT_POLL_INTERVAL_MS; - const maxWaitTimeMs = parsePositiveInteger(params.max_wait_time_ms, "max_wait_time_ms") || DEFAULT_MAX_WAIT_TIME_MS; - const deadline = Date.now() + maxWaitTimeMs; - - let latestStatus = ""; - let latestReason = ""; - let remoteVideoUrl = ""; - let resultSeed = createdTask.seed; - - while (Date.now() <= deadline) { - const statusResult = await queryVideoStatus(createdTask.api_base_url, createdTask.request_id); - latestStatus = statusResult.status; - latestReason = statusResult.reason; - resultSeed = statusResult.seed !== null && statusResult.seed !== undefined ? statusResult.seed : resultSeed; - - const normalizedStatus = normalizeVideoStatus(statusResult.status); - if (normalizedStatus === "succeed") { - remoteVideoUrl = statusResult.video_url; - if (!remoteVideoUrl) { - throw new Error("视频任务已成功,但返回中未找到 results.videos[0].url。"); - } - break; - } - - if (normalizedStatus === "failed") { - throw new Error(`视频生成失败: ${latestReason || "接口未返回失败原因"}`); - } - - await Tools.System.sleep(pollIntervalMs); - } - - if (!remoteVideoUrl) { - throw new Error(`视频生成超时,最后状态为 ${latestStatus || "未知"}${latestReason ? `,原因:${latestReason}` : ""}`); - } - - const baseName = buildFileBaseName(prompt, params.file_name, "siliconflow_video"); - const extension = guessExtensionFromUrl(remoteVideoUrl, "mp4"); - const filePath = `${VIDEOS_DIR}/${baseName}.${extension}`; - const downloadResult = await Tools.Files.download(remoteVideoUrl, filePath); - if (!downloadResult.successful) { - throw new Error(`下载视频失败: ${downloadResult.details}`); - } - - const fileUri = `file://${filePath}`; - const markdownLink = `[点击查看生成的视频](${fileUri})`; - const htmlVideo = ``; - const hintLines: string[] = []; - hintLines.push(`视频已生成并下载到 ${VIDEOS_DIR}。`); - hintLines.push(`本地路径: ${filePath}`); - hintLines.push(""); - hintLines.push("后续回答如果要给出视频,请优先返回这个本地链接:"); - hintLines.push(markdownLink); - hintLines.push(""); - hintLines.push("如果当前渲染环境支持 HTML 视频标签,也可以使用:"); - hintLines.push(htmlVideo); - - return { - prompt, - model: createdTask.effective_model, - request_id: createdTask.request_id, - seed: resultSeed, - file_path: filePath, - file_uri: fileUri, - remote_video_url: remoteVideoUrl, - markdown_link: markdownLink, - html_video: htmlVideo, - hint: hintLines.join("\n") - }; - } - - async function draw_image_wrapper(params: ImageParams) { - try { - const result = await draw_image(params || {}); - complete({ - success: true, - message: "SiliconFlow 图片生成成功,已下载到本地。", - data: result - }); - } catch (error) { - console.error("draw_image 执行失败:", error); - complete({ - success: false, - message: `SiliconFlow 图片生成失败: ${getErrorMessage(error)}`, - error_stack: error && error.stack ? error.stack : undefined - }); - } - } - - async function draw_video_wrapper(params: VideoParams) { - try { - const result = await draw_video(params || {}); - complete({ - success: true, - message: "SiliconFlow 视频生成成功,已下载到本地。", - data: result - }); - } catch (error) { - console.error("draw_video 执行失败:", error); - complete({ - success: false, - message: `SiliconFlow 视频生成失败: ${getErrorMessage(error)}`, - error_stack: error && error.stack ? error.stack : undefined - }); - } - } - - return { - draw_image: draw_image_wrapper, - draw_video: draw_video_wrapper - }; -})(); - -exports.draw_image = siliconflowDraw.draw_image; -exports.draw_video = siliconflowDraw.draw_video; diff --git a/scripts/super_admin.ts b/scripts/super_admin.ts deleted file mode 100644 index d6d42fb..0000000 --- a/scripts/super_admin.ts +++ /dev/null @@ -1,423 +0,0 @@ -/* -METADATA -{ - "name": "super_admin", - - "display_name": { - "zh": "超级管理员", - "en": "Super Admin" - }, - "description": { "zh": "超级管理员工具集,提供终端命令和Shell操作的高级功能。terminal工具运行在Ubuntu环境中(已正确挂载sdcard和storage),shell工具通过Shizuku/Root直接执行Android系统命令。适合需要进行底层系统管理和命令行操作的场景。", "en": "Super admin toolkit providing advanced terminal and shell capabilities. The terminal tool runs in an Ubuntu environment (with sdcard/storage mounted). The shell tool executes Android system commands directly via Shizuku/Root. Useful for low-level system administration and CLI operations." }, - "enabledByDefault": true, - "category": "System", - "tools": [ - { - "name": "terminal", - "description": { "zh": "在Ubuntu环境中执行命令并收集输出结果。运行环境:完整的Ubuntu系统,已正确挂载sdcard和storage目录,可访问Android存储空间。所有命令将会在相同的会话执行且上下文连贯。强烈建议每次都显式传 timeoutMs,避免命令卡住。禁止使用 `set -e`、`set -o errexit` 等会改变 shell 退出行为的命令,这会导致终端会话直接退出并卡死。若未传,前台默认15秒超时;background=true 时不使用该默认超时。命令超时时不会被自动取消,不需要重新执行命令,请继续通过 terminal_getscreen 跟踪当前屏幕内容。", "en": "Execute commands in an Ubuntu environment and collect output. Environment: full Ubuntu system with sdcard/storage mounted, allowing access to Android storage. Automatically preserves working-directory context. Strongly recommend explicitly passing timeoutMs every time to avoid hangs. Do not use commands such as `set -e` or `set -o errexit` that change shell exit behavior, because they can cause the terminal session to exit and hang. If omitted, foreground mode defaults to 15s timeout; background=true does not use this default timeout. When a command times out, it is not automatically cancelled. Do not rerun the command; continue tracking the current screen via terminal_getscreen." }, - "parameters": [ - { - "name": "command", - "description": { "zh": "要执行的命令", "en": "Command to execute." }, - "type": "string", - "required": true - }, - { - "name": "background", - "description": { "zh": "是否在后台运行命令,\"true\" 表示后台执行并立即返回,适合启动服务器等长时间运行的任务(AI 不会收到该命令的输出结果),\"false\" 或未提供则前台执行并等待并返回命令结果", "en": "Run command in background. 'true' runs in background and returns immediately (good for long-running tasks like servers; AI will not receive output). 'false' or omitted runs in foreground and returns the command result." }, - "type": "string", - "required": false - }, - { - "name": "timeoutMs", - "description": { "zh": "可选超时(毫秒,最低3000ms)。强烈建议显式传入;未传时前台默认15000ms,background=true时不使用默认超时。", "en": "Optional timeout (ms, minimum 3000ms). Strongly recommended to pass explicitly; if omitted, foreground defaults to 15000ms, and background=true does not use the default timeout." }, - "type": "string", - "required": false - } - ] - }, - { - "name": "terminal_wait", - "description": { "zh": "等待同一终端会话中的上一条命令执行完成。适用于安装/编译等长命令在 timeout 后继续后台执行的场景。与 sleep 不同,本工具会在命令实际完成时提前返回,而不是固定睡眠。", "en": "Wait until the previous command in the same terminal session finishes. Useful when long install/build commands continue running after a timeout. Unlike sleep, this tool can return early as soon as the command actually completes." }, - "parameters": [ - { - "name": "sessionId", - "description": { "zh": "可选目标会话ID。不传则使用当前对话的默认会话;无 chatId 时为 super_admin_default_session。", "en": "Optional target session ID. If omitted, uses the current chat's default session; without chatId it is super_admin_default_session." }, - "type": "string", - "required": false - }, - { - "name": "timeoutMs", - "description": { "zh": "可选超时(毫秒,最低3000ms)。未传时默认300000ms(5分钟)。", "en": "Optional timeout (ms, minimum 3000ms). Defaults to 300000ms (5 minutes) if omitted." }, - "type": "string", - "required": false - } - ] - }, - { - "name": "terminal_getscreen", - "description": { "zh": "获取当前终端会话可见屏幕内容(仅一屏,不包含历史滚动缓冲)。", "en": "Get the current visible screen content for the active terminal session (single screen only, no scrollback history)." }, - "parameters": [] - }, - { - "name": "terminal_input", - "description": { "zh": "向当前终端会话写入输入。input 与 control 至少传一个。常见用法:先写 input,再写 control=enter 提交;control=ctrl 且 input=c 可发送 Ctrl+C。", "en": "Write input to the active terminal session. Provide at least one of input or control. Typical usage: send input first, then control=enter to submit; use control=ctrl with input=c for Ctrl+C." }, - "parameters": [ - { - "name": "input", - "description": { "zh": "写入终端的文本", "en": "Text to write to terminal." }, - "type": "string", - "required": false - }, - { - "name": "control", - "description": { "zh": "控制键,例如 enter / tab / esc / ctrl", "en": "Control key, e.g. enter / tab / esc / ctrl." }, - "type": "string", - "required": false - } - ] - }, - { - "name": "shell", - "description": { "zh": "通过Shizuku/Root权限直接在Android系统中执行Shell命令。运行环境:直接访问Android系统,具有系统级权限,适用于需要操作Android系统底层的场景(如pm、am等系统命令)。", "en": "Execute shell commands directly on Android with Shizuku/Root. Environment: direct Android system access with system-level privileges, suitable for low-level commands such as pm/am." }, - "parameters": [ - { - "name": "command", - "description": { "zh": "要执行的Shell命令", "en": "Shell command to execute." }, - "type": "string", - "required": true - } - ] - } - ] -}*/ - -const superAdmin = (function () { - const MAX_INLINE_TERMINAL_OUTPUT_CHARS = 12_000; - const DEFAULT_FOREGROUND_TIMEOUT_MS = 15_000; - const DEFAULT_WAIT_TIMEOUT_MS = 300_000; - const MIN_TIMEOUT_MS = 3_000; - const DEFAULT_TERMINAL_SESSION_NAME = "super_admin_default_session"; - const BACKGROUND_TERMINAL_SESSION_PREFIX = "super_admin_background"; - - function getCurrentChatSessionSuffix(): string { - const chatId = getChatId(); - if (chatId === undefined) { - return ""; - } - const normalizedChatId = chatId.trim(); - if (!normalizedChatId) { - return ""; - } - return normalizedChatId.replace(/[^a-zA-Z0-9._-]+/g, "_"); - } - - function getDefaultTerminalSessionName(): string { - const chatSuffix = getCurrentChatSessionSuffix(); - return chatSuffix - ? `${DEFAULT_TERMINAL_SESSION_NAME}_${chatSuffix}` - : DEFAULT_TERMINAL_SESSION_NAME; - } - - function getBackgroundTerminalSessionName(): string { - const chatSuffix = getCurrentChatSessionSuffix(); - const prefix = chatSuffix - ? `${BACKGROUND_TERMINAL_SESSION_PREFIX}_${chatSuffix}` - : BACKGROUND_TERMINAL_SESSION_PREFIX; - return `${prefix}_${Date.now()}`; - } - - async function persistTerminalOutputIfTooLong(command: string, result: any): Promise { - const outputStr = typeof result?.output === "string" - ? result.output - : String(result?.output ?? ""); - - if (outputStr.length <= MAX_INLINE_TERMINAL_OUTPUT_CHARS) { - return null; - } - - await Tools.Files.mkdir(OPERIT_CLEAN_ON_EXIT_DIR, true); - - const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); - const rand = Math.floor(Math.random() * 1_000_000); - const filePath = `${OPERIT_CLEAN_ON_EXIT_DIR}/terminal_output_${timestamp}_${rand}.log`; - - await Tools.Files.write(filePath, outputStr, false); - - return { - command, - output: "(saved_to_file)", - exitCode: result?.exitCode, - sessionId: result?.sessionId, - context_preserved: true, - output_saved_to: filePath, - output_chars: outputStr.length, - operit_clean_on_exit_dir: OPERIT_CLEAN_ON_EXIT_DIR, - hint: "Output is large and saved to file. Use read_file_part or grep_code to inspect it.", - }; - } - - /** - * 在Ubuntu环境中执行终端命令并收集输出结果 - * 运行环境:完整的Ubuntu系统,已正确挂载sdcard和storage目录 - * 禁止使用 set -e / set -o errexit 等会改变 shell 退出行为的命令,否则可能导致终端会话退出并卡死 - * @param command - 要执行的命令 - * @param background - 是否后台运行("true" 为后台执行并立即返回,适合启动服务器等长时间运行任务,AI 不会收到该命令的输出结果) - * @param timeoutMs - 可选的超时时间(毫秒,最低 3000ms)。强烈建议显式传入;前台未传时默认 15000ms,后台模式不应用该默认值。 - */ - async function terminal(params: { command: string, background?: string, timeoutMs?: string }): Promise { - try { - if (!params.command) { - throw new Error("命令不能为空"); - } - - const command = params.command; - const background = params.background; - const timeoutMs = params.timeoutMs; - - console.log(`执行终端命令: ${command}`); - - const isBackground = background === "true"; - let timeout: number | undefined; - if (!isBackground) { - if (timeoutMs !== undefined) { - const parsedTimeout = parseInt(timeoutMs, 10); - if (!Number.isFinite(parsedTimeout) || parsedTimeout < MIN_TIMEOUT_MS) { - throw new Error(`timeoutMs必须是整数且不少于${MIN_TIMEOUT_MS}毫秒`); - } - timeout = parsedTimeout; - } else { - timeout = DEFAULT_FOREGROUND_TIMEOUT_MS; - } - } - - if (isBackground) { - const session = await Tools.System.terminal.create(getBackgroundTerminalSessionName()); - const sessionId = session.sessionId; - - // 调用系统工具执行终端命令 - (async () => { - try { - await Tools.System.terminal.exec(sessionId, command); - } catch (error) { - console.error(`[terminal/background] 错误: ${error.message}`); - console.error(error.stack); - } - })(); - - return { - command: command, - background: true, - sessionId: sessionId, - started: true - }; - } - - // 创建或获取一个默认会话 - const session = await Tools.System.terminal.create(getDefaultTerminalSessionName()); - const sessionId = session.sessionId; - - // 调用系统工具执行终端命令 - const result = await Tools.System.terminal.exec(sessionId, command, timeout); - const timedOut = result.timedOut === true; - - let timeoutScreen: { sessionId: string, rows: number, cols: number, content: string } | null = null; - if (timedOut) { - const screenResult = await Tools.System.terminal.screen(sessionId); - timeoutScreen = { - sessionId: screenResult.sessionId ?? sessionId, - rows: screenResult.rows, - cols: screenResult.cols, - content: screenResult.content - }; - } - - const persistedResult = await persistTerminalOutputIfTooLong(command, result); - if (persistedResult) { - persistedResult.timeoutMsUsed = timeout; - if (timeoutScreen) { - persistedResult.timeoutScreen = timeoutScreen; - } - return persistedResult; - } - - return { - command: command, - output: result.output, - exitCode: result.exitCode, - sessionId: result.sessionId, - timedOut: timedOut, - timeoutMsUsed: timeout, - timeoutScreen: timeoutScreen, - context_preserved: true // 标记此命令保留了目录上下文 - }; - } catch (error) { - console.error(`[terminal] 错误: ${error.message}`); - console.error(error.stack); - - throw error; - } - } - - /** - * 等待同一终端会话中的上一条命令执行完成 - * 原理:向同会话追加一个内部 marker 命令。由于会话按序执行,marker 开始执行即代表前序命令已完成。 - * @param sessionId - 可选会话ID;不传时使用当前对话的默认会话,无 chatId 时为 super_admin_default_session - * @param timeoutMs - 可选超时(毫秒,最低 3000ms);未传默认 300000ms - */ - async function terminal_wait(params: { sessionId?: string, timeoutMs?: string } = {}): Promise { - try { - const timeoutMs = params.timeoutMs; - let timeout = DEFAULT_WAIT_TIMEOUT_MS; - if (timeoutMs !== undefined) { - const parsedTimeout = parseInt(timeoutMs, 10); - if (!Number.isFinite(parsedTimeout) || parsedTimeout < MIN_TIMEOUT_MS) { - throw new Error(`timeoutMs必须是整数且不少于${MIN_TIMEOUT_MS}毫秒`); - } - timeout = parsedTimeout; - } - - const session = - params.sessionId - ? { sessionId: params.sessionId } - : await Tools.System.terminal.create(getDefaultTerminalSessionName()); - const sessionId = session.sessionId; - - const marker = `__OPERIT_TERMINAL_WAIT_DONE_${Date.now()}_${Math.floor(Math.random() * 1_000_000)}__`; - const waitCommand = `printf '${marker}\\n'`; - const startedAt = Date.now(); - const result = await Tools.System.terminal.exec(sessionId, waitCommand, timeout); - const elapsedMs = Date.now() - startedAt; - const timedOut = result?.timedOut === true; - - let timeoutScreen: { sessionId: string, rows: number, cols: number, content: string } | null = null; - if (timedOut) { - const screenResult = await Tools.System.terminal.screen(sessionId); - timeoutScreen = { - sessionId: screenResult.sessionId ?? sessionId, - rows: screenResult.rows, - cols: screenResult.cols, - content: screenResult.content - }; - } - - const outputStr = typeof result?.output === "string" - ? result.output - : String(result?.output ?? ""); - const markerSeen = outputStr.includes(marker); - - return { - sessionId, - timedOut, - timeoutMsUsed: timeout, - elapsedMs, - waitCompleted: !timedOut && markerSeen, - markerSeen, - exitCode: result?.exitCode, - timeoutScreen, - context_preserved: true - }; - } catch (error) { - console.error(`[terminal_wait] 错误: ${error.message}`); - console.error(error.stack); - throw error; - } - } - - /** - * 通过Shizuku/Root权限在Android系统中执行Shell命令 - * 运行环境:直接访问Android系统,具有系统级权限 - * @param command - 要执行的Shell命令 - */ - async function shell(params: { command: string }): Promise { - try { - if (!params.command) { - throw new Error("命令不能为空"); - } - const command = params.command; - - console.log(`执行Shell命令: ${command}`); - - // 通过Shizuku/Root权限执行shell操作 - const result = await Tools.System.shell(`${command}`); - - return { - command: command, - output: result.output, - exitCode: result.exitCode - }; - } catch (error) { - console.error(`[shell] 错误: ${error.message}`); - console.error(error.stack); - - throw error; - } - } - - /** - * 获取当前终端会话可见屏幕内容(仅一屏,不包含历史) - */ - async function terminal_getscreen(_params: {} = {}): Promise { - try { - const session = await Tools.System.terminal.create(getDefaultTerminalSessionName()); - const sessionId = session.sessionId; - const result = await Tools.System.terminal.screen(sessionId); - return { - sessionId: result.sessionId ?? sessionId, - rows: result.rows, - cols: result.cols, - content: result.content, - commandRunning: result.commandRunning === true - }; - } catch (error) { - console.error(`[terminal_getscreen] 错误: ${error.message}`); - console.error(error.stack); - throw error; - } - } - - /** - * 向当前终端会话写入输入 - * @param input - 文本输入 - * @param control - 控制键 - */ - async function terminal_input(params: { input?: string, control?: string } = {}): Promise { - try { - if (params.input === undefined && params.control === undefined) { - throw new Error("input和control至少需要提供一个"); - } - - const session = await Tools.System.terminal.create(getDefaultTerminalSessionName()); - const sessionId = session.sessionId; - const result = await Tools.System.terminal.input(sessionId, { - input: params.input, - control: params.control - }); - - return { - sessionId: sessionId, - input: params.input, - control: params.control, - result: result?.value ?? String(result ?? "") - }; - } catch (error) { - console.error(`[terminal_input] 错误: ${error.message}`); - console.error(error.stack); - throw error; - } - } - - return { - terminal, - terminal_wait, - terminal_getscreen, - terminal_input, - shell - }; -})(); - -// 逐个导出 -exports.terminal = superAdmin.terminal; -exports.terminal_wait = superAdmin.terminal_wait; -exports.terminal_getscreen = superAdmin.terminal_getscreen; -exports.terminal_input = superAdmin.terminal_input; -exports.shell = superAdmin.shell; diff --git a/scripts/system_tools.ts b/scripts/system_tools.ts deleted file mode 100644 index ba0218a..0000000 --- a/scripts/system_tools.ts +++ /dev/null @@ -1,774 +0,0 @@ -/* METADATA -{ - "name": "system_tools", - - "display_name": { - "zh": "系统工具", - "en": "System Tools" - }, - "description": { - "zh": "提供系统级操作工具,包括设置管理、应用安装卸载与启动、通知获取、位置服务、设备信息查询,以及 Intent/广播调用。", - "en": "System-level operations: settings management, app install/uninstall & launch, notification retrieval, location services, device info queries, plus Intent/broadcast execution." - }, - "enabledByDefault": true, - "category": "System", - "tools": [ - { - "name": "get_system_setting", - "description": { "zh": "获取系统设置的值。需要用户授权。", "en": "Get the value of a system setting. Requires user authorization." }, - "parameters": [ - { "name": "setting", "description": { "zh": "设置名称", "en": "Setting key/name" }, "type": "string", "required": true }, - { "name": "namespace", "description": { "zh": "命名空间:system/secure/global,默认system", "en": "Namespace: system/secure/global (default: system)" }, "type": "string", "required": false } - ] - }, - { - "name": "modify_system_setting", - "description": { "zh": "修改系统设置的值。需要用户授权。", "en": "Modify the value of a system setting. Requires user authorization." }, - "parameters": [ - { "name": "setting", "description": { "zh": "设置名称", "en": "Setting key/name" }, "type": "string", "required": true }, - { "name": "value", "description": { "zh": "设置值", "en": "Setting value" }, "type": "string", "required": true }, - { "name": "namespace", "description": { "zh": "命名空间:system/secure/global,默认system", "en": "Namespace: system/secure/global (default: system)" }, "type": "string", "required": false } - ] - }, - { - "name": "install_app", - "description": { "zh": "安装应用程序。需要用户授权。", "en": "Install an app. Requires user authorization." }, - "parameters": [ - { "name": "path", "description": { "zh": "APK文件路径", "en": "APK file path" }, "type": "string", "required": true } - ] - }, - { - "name": "uninstall_app", - "description": { "zh": "卸载应用程序。需要用户授权。", "en": "Uninstall an app. Requires user authorization." }, - "parameters": [ - { "name": "package_name", "description": { "zh": "应用包名", "en": "App package name" }, "type": "string", "required": true }, - { "name": "keep_data", "description": { "zh": "是否保留数据,默认false", "en": "Whether to keep app data (default: false)" }, "type": "boolean", "required": false } - ] - }, - { - "name": "list_installed_apps", - "description": { "zh": "获取已安装应用程序列表。需要用户授权。", "en": "List installed apps. Requires user authorization." }, - "parameters": [ - { "name": "include_system_apps", "description": { "zh": "是否包含系统应用,默认false", "en": "Whether to include system apps (default: false)" }, "type": "boolean", "required": false } - ] - }, - { - "name": "start_app", - "description": { "zh": "启动应用程序。需要用户授权。", "en": "Launch an app. Requires user authorization." }, - "parameters": [ - { "name": "package_name", "description": { "zh": "应用包名", "en": "App package name" }, "type": "string", "required": true }, - { "name": "activity", "description": { "zh": "可选活动名称", "en": "Optional activity name" }, "type": "string", "required": false } - ] - }, - { - "name": "stop_app", - "description": { "zh": "停止正在运行的应用程序。需要用户授权。", "en": "Force stop a running app. Requires user authorization." }, - "parameters": [ - { "name": "package_name", "description": { "zh": "应用包名", "en": "App package name" }, "type": "string", "required": true } - ] - }, - { - "name": "send_broadcast", - "description": { "zh": "发送广播(Broadcast Intent)。需要用户授权。", "en": "Send a broadcast (Broadcast Intent). Requires user authorization." }, - "parameters": [ - { "name": "action", "description": { "zh": "Intent action,例如 android.intent.action.VIEW", "en": "Intent action, e.g. android.intent.action.VIEW" }, "type": "string", "required": true }, - { "name": "package_name", "description": { "zh": "可选:限制广播目标包名", "en": "Optional: restrict target package" }, "type": "string", "required": false }, - { "name": "component", "description": { "zh": "可选:组件名 package/class,优先于package_name", "en": "Optional: component package/class, takes priority over package_name" }, "type": "string", "required": false }, - { "name": "uri", "description": { "zh": "可选:data uri", "en": "Optional: data uri" }, "type": "string", "required": false }, - { "name": "extras", "description": { "zh": "可选:extras(对象,可用于传参)", "en": "Optional: extras (object for parameters)" }, "type": "object", "required": false } - ] - }, - { - "name": "execute_intent", - "description": { "zh": "执行 Intent(Activity/Service/Broadcast),支持 extras 传参。需要用户授权。", "en": "Execute an Intent (Activity/Service/Broadcast) with extras parameters. Requires user authorization." }, - "parameters": [ - { "name": "type", "description": { "zh": "类型:activity/broadcast/service,默认activity", "en": "Type: activity/broadcast/service (default: activity)" }, "type": "string", "required": false }, - { "name": "action", "description": { "zh": "Intent action(action 或 component 至少一个必填)", "en": "Intent action (either action or component is required)" }, "type": "string", "required": false }, - { "name": "package_name", "description": { "zh": "可选:包名", "en": "Optional: package name" }, "type": "string", "required": false }, - { "name": "component", "description": { "zh": "可选:组件名 package/class", "en": "Optional: component package/class" }, "type": "string", "required": false }, - { "name": "uri", "description": { "zh": "可选:data uri", "en": "Optional: data uri" }, "type": "string", "required": false }, - { "name": "flags", "description": { "zh": "可选:flags(整数或JSON数组字符串)", "en": "Optional: flags (integer or JSON array string)" }, "type": "string", "required": false }, - { "name": "extras", "description": { "zh": "可选:extras(对象,可用于传参)", "en": "Optional: extras (object for parameters)" }, "type": "object", "required": false } - ] - }, - { - "name": "get_notifications", - "description": { "zh": "获取设备通知内容。", "en": "Retrieve device notifications." }, - "parameters": [ - { "name": "limit", "description": { "zh": "最大返回条数,默认10", "en": "Max number of entries to return (default: 10)" }, "type": "number", "required": false }, - { "name": "include_ongoing", "description": { "zh": "是否包含常驻通知,默认false", "en": "Whether to include ongoing notifications (default: false)" }, "type": "boolean", "required": false } - ] - }, - { - "name": "get_app_usage_time", - "description": { "zh": "获取应用前台使用时长。需要授予“使用情况访问权限”。", "en": "Get app foreground usage time. Requires Usage Access permission." }, - "parameters": [ - { "name": "package_name", "description": { "zh": "可选:精确应用包名", "en": "Optional exact package name" }, "type": "string", "required": false }, - { "name": "since_hours", "description": { "zh": "向前统计多少小时,默认24", "en": "How many hours to look back (default: 24)" }, "type": "number", "required": false }, - { "name": "limit", "description": { "zh": "不传包名时最多返回多少个应用,默认10", "en": "Max apps to return when package_name is omitted (default: 10)" }, "type": "number", "required": false }, - { "name": "include_system_apps", "description": { "zh": "不传包名时是否包含系统应用,默认false", "en": "Whether to include system apps when package_name is omitted (default: false)" }, "type": "boolean", "required": false } - ] - }, - { - "name": "get_device_location", - "description": { "zh": "获取设备当前位置信息。", "en": "Get current device location." }, - "parameters": [ - { "name": "high_accuracy", "description": { "zh": "是否使用高精度模式,默认false", "en": "Use high accuracy mode (default: false)" }, "type": "boolean", "required": false }, - { "name": "timeout", "description": { "zh": "超时时间(秒),默认10", "en": "Timeout in seconds (default: 10)" }, "type": "number", "required": false } - ] - }, - { - "name": "request_bluetooth_permission", - "description": { "zh": "请求蓝牙附近设备权限。", "en": "Request Bluetooth nearby devices permission." }, - "parameters": [] - }, - { - "name": "get_bluetooth_state", - "description": { "zh": "获取蓝牙适配器状态。", "en": "Get Bluetooth adapter state." }, - "parameters": [] - }, - { - "name": "request_enable_bluetooth", - "description": { "zh": "打开系统蓝牙开启对话框。", "en": "Open the system dialog to enable Bluetooth." }, - "parameters": [] - }, - { - "name": "list_bluetooth_bonded_devices", - "description": { "zh": "列出已配对蓝牙设备。", "en": "List bonded Bluetooth devices." }, - "parameters": [] - }, - { - "name": "scan_bluetooth_devices", - "description": { "zh": "扫描附近蓝牙 Classic 与 BLE 设备。", "en": "Scan nearby Bluetooth classic and BLE devices." }, - "parameters": [ - { "name": "duration_ms", "description": { "zh": "扫描时长毫秒。", "en": "Scan duration in milliseconds." }, "type": "number", "required": false }, - { "name": "include_ble", "description": { "zh": "是否包含 BLE 扫描。", "en": "Whether to include BLE scanning." }, "type": "boolean", "required": false } - ] - }, - { - "name": "bluetooth_connect", - "description": { "zh": "连接蓝牙 Classic 设备。", "en": "Connect to a Bluetooth classic device." }, - "parameters": [ - { "name": "address", "description": { "zh": "蓝牙 MAC 地址", "en": "Bluetooth MAC address" }, "type": "string", "required": true }, - { "name": "uuid", "description": { "zh": "RFCOMM UUID。", "en": "RFCOMM UUID." }, "type": "string", "required": false } - ] - }, - { - "name": "bluetooth_listen", - "description": { "zh": "监听别人连接本机的蓝牙 Classic 通道。", "en": "Listen for another device connecting to this phone over Bluetooth classic." }, - "parameters": [ - { "name": "name", "description": { "zh": "服务名。", "en": "Service name." }, "type": "string", "required": false }, - { "name": "uuid", "description": { "zh": "RFCOMM UUID。", "en": "RFCOMM UUID." }, "type": "string", "required": false } - ] - }, - { - "name": "bluetooth_accept", - "description": { "zh": "接受蓝牙 Classic 监听会话中的一个传入连接。", "en": "Accept one incoming connection from a Bluetooth classic listener." }, - "parameters": [ - { "name": "listener_session_id", "description": { "zh": "监听会话 ID", "en": "Listener session ID" }, "type": "string", "required": true }, - { "name": "timeout_ms", "description": { "zh": "等待毫秒数。", "en": "Wait time in milliseconds." }, "type": "number", "required": false } - ] - }, - { - "name": "bluetooth_send", - "description": { "zh": "向蓝牙 Classic 会话发送文本或 base64 字节。", "en": "Send text or base64 bytes to a Bluetooth classic session." }, - "parameters": [ - { "name": "session_id", "description": { "zh": "会话 ID", "en": "Session ID" }, "type": "string", "required": true }, - { "name": "text", "description": { "zh": "UTF-8 文本", "en": "UTF-8 text" }, "type": "string", "required": false }, - { "name": "data_base64", "description": { "zh": "base64 字节", "en": "Base64 bytes" }, "type": "string", "required": false } - ] - }, - { - "name": "bluetooth_read", - "description": { "zh": "从蓝牙 Classic 会话读取文本或字节。", "en": "Read text or bytes from a Bluetooth classic session." }, - "parameters": [ - { "name": "session_id", "description": { "zh": "会话 ID", "en": "Session ID" }, "type": "string", "required": true }, - { "name": "max_bytes", "description": { "zh": "最大读取字节数。", "en": "Maximum bytes to read." }, "type": "number", "required": false }, - { "name": "timeout_ms", "description": { "zh": "等待毫秒数。", "en": "Wait time in milliseconds." }, "type": "number", "required": false } - ] - }, - { - "name": "bluetooth_send_and_read", - "description": { "zh": "向蓝牙 Classic 会话发送后读取响应。", "en": "Send to a Bluetooth classic session and read the response." }, - "parameters": [ - { "name": "session_id", "description": { "zh": "会话 ID", "en": "Session ID" }, "type": "string", "required": true }, - { "name": "text", "description": { "zh": "UTF-8 文本", "en": "UTF-8 text" }, "type": "string", "required": false }, - { "name": "data_base64", "description": { "zh": "base64 字节", "en": "Base64 bytes" }, "type": "string", "required": false }, - { "name": "max_bytes", "description": { "zh": "最大读取字节数。", "en": "Maximum bytes to read." }, "type": "number", "required": false }, - { "name": "timeout_ms", "description": { "zh": "等待毫秒数。", "en": "Wait time in milliseconds." }, "type": "number", "required": false } - ] - }, - { - "name": "bluetooth_close", - "description": { "zh": "关闭蓝牙 Classic、监听或 BLE 会话。", "en": "Close a Bluetooth classic, listener, or BLE session." }, - "parameters": [ - { "name": "session_id", "description": { "zh": "会话 ID", "en": "Session ID" }, "type": "string", "required": true } - ] - }, - { - "name": "bluetooth_ble_connect", - "description": { "zh": "连接 BLE 设备。", "en": "Connect to a BLE device." }, - "parameters": [ - { "name": "address", "description": { "zh": "蓝牙 MAC 地址", "en": "Bluetooth MAC address" }, "type": "string", "required": true }, - { "name": "auto_connect", "description": { "zh": "是否自动连接。", "en": "Whether to use auto connect." }, "type": "boolean", "required": false } - ] - }, - { - "name": "bluetooth_ble_discover_services", - "description": { "zh": "发现 BLE 服务和 characteristic。", "en": "Discover BLE services and characteristics." }, - "parameters": [ - { "name": "session_id", "description": { "zh": "BLE 会话 ID", "en": "BLE session ID" }, "type": "string", "required": true }, - { "name": "timeout_ms", "description": { "zh": "等待毫秒数。", "en": "Wait time in milliseconds." }, "type": "number", "required": false } - ] - }, - { - "name": "bluetooth_ble_read_characteristic", - "description": { "zh": "读取 BLE characteristic。", "en": "Read a BLE characteristic." }, - "parameters": [ - { "name": "session_id", "description": { "zh": "BLE 会话 ID", "en": "BLE session ID" }, "type": "string", "required": true }, - { "name": "service_uuid", "description": { "zh": "Service UUID", "en": "Service UUID" }, "type": "string", "required": true }, - { "name": "characteristic_uuid", "description": { "zh": "Characteristic UUID", "en": "Characteristic UUID" }, "type": "string", "required": true }, - { "name": "timeout_ms", "description": { "zh": "等待毫秒数。", "en": "Wait time in milliseconds." }, "type": "number", "required": false } - ] - }, - { - "name": "bluetooth_ble_write_characteristic", - "description": { "zh": "写入 BLE characteristic。", "en": "Write a BLE characteristic." }, - "parameters": [ - { "name": "session_id", "description": { "zh": "BLE 会话 ID", "en": "BLE session ID" }, "type": "string", "required": true }, - { "name": "service_uuid", "description": { "zh": "Service UUID", "en": "Service UUID" }, "type": "string", "required": true }, - { "name": "characteristic_uuid", "description": { "zh": "Characteristic UUID", "en": "Characteristic UUID" }, "type": "string", "required": true }, - { "name": "text", "description": { "zh": "UTF-8 文本", "en": "UTF-8 text" }, "type": "string", "required": false }, - { "name": "data_base64", "description": { "zh": "base64 字节", "en": "Base64 bytes" }, "type": "string", "required": false } - ] - }, - { - "name": "bluetooth_ble_write_and_read_characteristic", - "description": { "zh": "写入 BLE characteristic 后读取另一个 characteristic 响应。", "en": "Write a BLE characteristic and read another characteristic response." }, - "parameters": [ - { "name": "session_id", "description": { "zh": "BLE 会话 ID", "en": "BLE session ID" }, "type": "string", "required": true }, - { "name": "write_service_uuid", "description": { "zh": "写入 Service UUID", "en": "Write service UUID" }, "type": "string", "required": true }, - { "name": "write_characteristic_uuid", "description": { "zh": "写入 Characteristic UUID", "en": "Write characteristic UUID" }, "type": "string", "required": true }, - { "name": "read_service_uuid", "description": { "zh": "读取 Service UUID", "en": "Read service UUID" }, "type": "string", "required": true }, - { "name": "read_characteristic_uuid", "description": { "zh": "读取 Characteristic UUID", "en": "Read characteristic UUID" }, "type": "string", "required": true }, - { "name": "text", "description": { "zh": "UTF-8 文本", "en": "UTF-8 text" }, "type": "string", "required": false }, - { "name": "data_base64", "description": { "zh": "base64 字节", "en": "Base64 bytes" }, "type": "string", "required": false }, - { "name": "timeout_ms", "description": { "zh": "等待毫秒数。", "en": "Wait time in milliseconds." }, "type": "number", "required": false } - ] - }, - { - "name": "bluetooth_ble_subscribe_characteristic", - "description": { "zh": "订阅或取消订阅 BLE characteristic 通知。", "en": "Subscribe or unsubscribe BLE characteristic notifications." }, - "parameters": [ - { "name": "session_id", "description": { "zh": "BLE 会话 ID", "en": "BLE session ID" }, "type": "string", "required": true }, - { "name": "service_uuid", "description": { "zh": "Service UUID", "en": "Service UUID" }, "type": "string", "required": true }, - { "name": "characteristic_uuid", "description": { "zh": "Characteristic UUID", "en": "Characteristic UUID" }, "type": "string", "required": true }, - { "name": "enable", "description": { "zh": "是否订阅。", "en": "Whether to subscribe." }, "type": "boolean", "required": false } - ] - }, - { - "name": "bluetooth_ble_read_notifications", - "description": { "zh": "读取已收到的 BLE 通知。", "en": "Read received BLE notifications." }, - "parameters": [ - { "name": "session_id", "description": { "zh": "BLE 会话 ID", "en": "BLE session ID" }, "type": "string", "required": true }, - { "name": "limit", "description": { "zh": "读取条数。", "en": "Number of notifications to read." }, "type": "number", "required": false } - ] - }, - { - "name": "get_device_info", - "description": { "zh": "获取详细的设备信息,包括型号、操作系统版本、内存、存储、网络状态等。", "en": "Get detailed device information, including model, OS version, memory, storage, network status, etc." }, - "parameters": [] - } - ] -}*/ - -const SystemTools = (function () { - - interface ToolResponse { - success: boolean; - message: string; - data?: any; - } - - type BluetoothScanParams = { duration_ms?: number | string, include_ble?: boolean }; - type BluetoothConnectParams = { address: string, uuid?: string }; - type BluetoothListenParams = { name?: string, uuid?: string }; - type BluetoothAcceptParams = { listener_session_id: string, timeout_ms?: number | string }; - type BluetoothPayloadParams = { session_id: string, text?: string, data_base64?: string }; - type BluetoothReadParams = { session_id: string, max_bytes?: number | string, timeout_ms?: number | string }; - type BluetoothSendAndReadParams = BluetoothPayloadParams & { max_bytes?: number | string, timeout_ms?: number | string }; - type BluetoothCloseParams = { session_id: string }; - type BluetoothBleConnectParams = { address: string, auto_connect?: boolean }; - type BluetoothBleDiscoverParams = { session_id: string, timeout_ms?: number | string }; - type BluetoothBleCharacteristicParams = { session_id: string, service_uuid: string, characteristic_uuid: string, timeout_ms?: number | string }; - type BluetoothBleWriteParams = { session_id: string, service_uuid: string, characteristic_uuid: string, text?: string, data_base64?: string }; - type BluetoothBleWriteAndReadParams = { - session_id: string, - write_service_uuid: string, - write_characteristic_uuid: string, - read_service_uuid: string, - read_characteristic_uuid: string, - text?: string, - data_base64?: string, - timeout_ms?: number | string - }; - type BluetoothBleSubscribeParams = { session_id: string, service_uuid: string, characteristic_uuid: string, enable?: boolean }; - type BluetoothBleReadNotificationsParams = { session_id: string, limit?: number | string }; - - async function get_system_setting(params: { setting: string, namespace?: string }): Promise { - const result = await Tools.System.getSetting(params.setting, params.namespace || 'system'); - return { success: true, message: '成功获取系统设置', data: result }; - } - - async function modify_system_setting(params: { setting: string, value: string, namespace?: string }): Promise { - const result = await Tools.System.setSetting(params.setting, params.value, params.namespace || 'system'); - const success = result && result.value === params.value; - return { success: success, message: success ? '成功修改系统设置' : '修改系统设置失败', data: result }; - } - - async function install_app(params: { path: string }): Promise { - const result = await Tools.System.installApp(params.path); - return { success: result.success, message: result.success ? '应用安装成功' : '应用安装失败', data: result }; - } - - async function uninstall_app(params: { package_name: string, keep_data?: boolean }): Promise { - const result = await Tools.System.uninstallApp(params.package_name); - return { success: result.success, message: result.success ? '应用卸载成功' : '应用卸载失败', data: result }; - } - - async function list_installed_apps(params: { include_system_apps?: boolean }): Promise { - const result = await Tools.System.listApps(params.include_system_apps || false); - return { success: true, message: '成功获取应用列表', data: result }; - } - - async function start_app(params: { package_name: string, activity?: string }): Promise { - const result = await Tools.System.startApp(params.package_name, params.activity); - return { success: result.success, message: result.success ? '应用启动成功' : '应用启动失败', data: result }; - } - - async function stop_app(params: { package_name: string }): Promise { - const result = await Tools.System.stopApp(params.package_name); - return { success: result.success, message: result.success ? '应用停止成功' : '应用停止失败', data: result }; - } - - async function send_broadcast(params: { action: string, package_name?: string, component?: string, uri?: string, extras?: any, extra_key?: string, extra_value?: string, extra_key2?: string, extra_value2?: string }): Promise { - const result = await Tools.System.sendBroadcast({ - action: params.action, - uri: params.uri, - package: params.package_name, - component: params.component, - extras: params.extras, - extra_key: params.extra_key, - extra_value: params.extra_value, - extra_key2: params.extra_key2, - extra_value2: params.extra_value2 - }); - return { success: true, message: '广播发送成功', data: result }; - } - - async function execute_intent(params: { type?: 'activity' | 'broadcast' | 'service' | string, action?: string, uri?: string, package_name?: string, component?: string, flags?: number | number[] | string, extras?: any }): Promise { - const flags = Array.isArray(params.flags) ? JSON.stringify(params.flags) : params.flags; - const result = await Tools.System.intent({ - action: params.action, - uri: params.uri, - package: params.package_name, - component: params.component, - flags: flags as any, - extras: params.extras, - type: (params.type as any) || 'activity' - }); - return { success: true, message: 'Intent 执行成功', data: result }; - } - - async function get_notifications(params: { limit?: number, include_ongoing?: boolean }): Promise { - const result = await Tools.System.getNotifications(params.limit || 10, params.include_ongoing || false); - return { success: true, message: '成功获取通知', data: result }; - } - - async function get_app_usage_time(params: { package_name?: string, since_hours?: number, limit?: number, include_system_apps?: boolean }): Promise { - const result = await Tools.System.getAppUsageTime({ - packageName: params.package_name, - sinceHours: params.since_hours || 24, - limit: params.limit || 10, - includeSystemApps: params.include_system_apps || false - }); - return { success: true, message: '成功获取应用使用时长', data: result }; - } - - async function get_device_location(params: { high_accuracy?: boolean, timeout?: number }): Promise { - const result = await Tools.System.getLocation(params.high_accuracy || false, params.timeout || 10); - return { success: true, message: '成功获取位置信息', data: result }; - } - - async function request_bluetooth_permission(params: Record): Promise { - const result = await Tools.System.bluetooth.requestPermission(); - return { success: true, message: '成功请求蓝牙权限', data: result }; - } - - async function get_bluetooth_state(params: Record): Promise { - const result = await Tools.System.bluetooth.getState(); - return { success: true, message: '成功获取蓝牙状态', data: result }; - } - - async function request_enable_bluetooth(params: Record): Promise { - const result = await Tools.System.bluetooth.requestEnable(); - return { success: true, message: '已打开蓝牙开启请求', data: result }; - } - - async function list_bluetooth_bonded_devices(params: Record): Promise { - const result = await Tools.System.bluetooth.listBondedDevices(); - return { success: true, message: '成功获取已配对蓝牙设备', data: result }; - } - - async function scan_bluetooth_devices(params: BluetoothScanParams): Promise { - const result = await Tools.System.bluetooth.scan({ - durationMs: params.duration_ms, - includeBle: params.include_ble - }); - return { success: true, message: '成功扫描蓝牙设备', data: result }; - } - - async function bluetooth_connect(params: BluetoothConnectParams): Promise { - const result = await Tools.System.bluetooth.connect({ - address: params.address, - uuid: params.uuid - }); - return { success: true, message: '成功连接蓝牙设备', data: result }; - } - - async function bluetooth_listen(params: BluetoothListenParams): Promise { - const result = await Tools.System.bluetooth.listen({ - name: params.name, - uuid: params.uuid - }); - return { success: true, message: '成功创建蓝牙监听', data: result }; - } - - async function bluetooth_accept(params: BluetoothAcceptParams): Promise { - const result = await Tools.System.bluetooth.accept(params.listener_session_id, params.timeout_ms); - return { success: true, message: '成功接受蓝牙连接', data: result }; - } - - async function bluetooth_send(params: BluetoothPayloadParams): Promise { - const result = await Tools.System.bluetooth.send(params.session_id, { - text: params.text, - dataBase64: params.data_base64 - }); - return { success: true, message: '成功发送蓝牙数据', data: result }; - } - - async function bluetooth_read(params: BluetoothReadParams): Promise { - const result = await Tools.System.bluetooth.read(params.session_id, { - maxBytes: params.max_bytes, - timeoutMs: params.timeout_ms - }); - return { success: true, message: '成功读取蓝牙数据', data: result }; - } - - async function bluetooth_send_and_read(params: BluetoothSendAndReadParams): Promise { - const result = await Tools.System.bluetooth.sendAndRead(params.session_id, { - text: params.text, - dataBase64: params.data_base64, - maxBytes: params.max_bytes, - timeoutMs: params.timeout_ms - }); - return { success: true, message: '成功发送并读取蓝牙数据', data: result }; - } - - async function bluetooth_close(params: BluetoothCloseParams): Promise { - const result = await Tools.System.bluetooth.close(params.session_id); - return { success: true, message: '成功关闭蓝牙会话', data: result }; - } - - async function bluetooth_ble_connect(params: BluetoothBleConnectParams): Promise { - const result = await Tools.System.bluetooth.ble.connect({ - address: params.address, - autoConnect: params.auto_connect - }); - return { success: true, message: '成功连接 BLE 设备', data: result }; - } - - async function bluetooth_ble_discover_services(params: BluetoothBleDiscoverParams): Promise { - const result = await Tools.System.bluetooth.ble.discoverServices(params.session_id, params.timeout_ms); - return { success: true, message: '成功发现 BLE 服务', data: result }; - } - - async function bluetooth_ble_read_characteristic(params: BluetoothBleCharacteristicParams): Promise { - const result = await Tools.System.bluetooth.ble.readCharacteristic(params.session_id, { - serviceUuid: params.service_uuid, - characteristicUuid: params.characteristic_uuid, - timeoutMs: params.timeout_ms - }); - return { success: true, message: '成功读取 BLE characteristic', data: result }; - } - - async function bluetooth_ble_write_characteristic(params: BluetoothBleWriteParams): Promise { - const result = await Tools.System.bluetooth.ble.writeCharacteristic(params.session_id, { - serviceUuid: params.service_uuid, - characteristicUuid: params.characteristic_uuid, - text: params.text, - dataBase64: params.data_base64 - }); - return { success: true, message: '成功写入 BLE characteristic', data: result }; - } - - async function bluetooth_ble_write_and_read_characteristic(params: BluetoothBleWriteAndReadParams): Promise { - const result = await Tools.System.bluetooth.ble.writeAndReadCharacteristic(params.session_id, { - writeServiceUuid: params.write_service_uuid, - writeCharacteristicUuid: params.write_characteristic_uuid, - readServiceUuid: params.read_service_uuid, - readCharacteristicUuid: params.read_characteristic_uuid, - text: params.text, - dataBase64: params.data_base64, - timeoutMs: params.timeout_ms - }); - return { success: true, message: '成功写入并读取 BLE characteristic', data: result }; - } - - async function bluetooth_ble_subscribe_characteristic(params: BluetoothBleSubscribeParams): Promise { - const result = await Tools.System.bluetooth.ble.subscribe(params.session_id, { - serviceUuid: params.service_uuid, - characteristicUuid: params.characteristic_uuid, - enable: params.enable - }); - return { success: true, message: '成功更新 BLE 订阅', data: result }; - } - - async function bluetooth_ble_read_notifications(params: BluetoothBleReadNotificationsParams): Promise { - const result = await Tools.System.bluetooth.ble.readNotifications(params.session_id, params.limit); - return { success: true, message: '成功读取 BLE 通知', data: result }; - } - - async function get_device_info(params: {}): Promise { - const result = await Tools.System.getDeviceInfo(); - return { success: true, message: '成功获取设备信息', data: result }; - } - - async function wrapToolExecution

(func: (params: P) => Promise, params: P) { - try { - const result = await func(params); - complete(result); - } catch (error: any) { - console.error(`Tool ${func.name} failed unexpectedly`, error); - complete({ - success: false, - message: `工具执行时发生意外错误: ${error.message}`, - }); - } - } - - async function main() { - console.log("=== System Tools 全面测试开始 ===\n"); - const results: any[] = []; - - try { - // 1. 测试 get_device_info(新增工具) - console.log("1. 测试 get_device_info..."); - try { - const deviceInfoResult = await get_device_info({}); - results.push({ tool: 'get_device_info', result: deviceInfoResult }); - console.log("✓ get_device_info 测试完成"); - if (deviceInfoResult.data) { - console.log(` 设备信息: ${JSON.stringify(deviceInfoResult.data).substring(0, 100)}...`); - } - console.log(); - } catch (error: any) { - console.log("⚠ get_device_info 测试失败:", error.message, "\n"); - results.push({ tool: 'get_device_info', result: { success: false, message: error.message } }); - } - - // 2. 测试 get_notifications - console.log("2. 测试 get_notifications..."); - try { - const notificationsResult = await get_notifications({ limit: 5, include_ongoing: false }); - results.push({ tool: 'get_notifications', result: notificationsResult }); - console.log("✓ get_notifications 测试完成"); - if (notificationsResult.data) { - console.log(` 获取到 ${notificationsResult.data.length || 0} 条通知`); - } - console.log(); - } catch (error: any) { - console.log("⚠ get_notifications 测试失败:", error.message, "\n"); - results.push({ tool: 'get_notifications', result: { success: false, message: error.message } }); - } - - // 3. 测试 get_app_usage_time - console.log("3. 测试 get_app_usage_time..."); - try { - const usageResult = await get_app_usage_time({ since_hours: 24, limit: 5, include_system_apps: false }); - results.push({ tool: 'get_app_usage_time', result: usageResult }); - console.log("✓ get_app_usage_time 测试完成"); - if (usageResult.data) { - console.log(` 返回条目数: ${usageResult.data.entries ? usageResult.data.entries.length : 0}`); - } - console.log(); - } catch (error: any) { - console.log("⚠ get_app_usage_time 测试失败(可能需要使用情况访问权限):", error.message, "\n"); - results.push({ tool: 'get_app_usage_time', result: { success: false, message: error.message } }); - } - - // 4. 测试 get_device_location - console.log("4. 测试 get_device_location..."); - try { - const locationResult = await get_device_location({ high_accuracy: false, timeout: 5 }); - results.push({ tool: 'get_device_location', result: locationResult }); - console.log("✓ get_device_location 测试完成"); - if (locationResult.data) { - console.log(` 位置: ${JSON.stringify(locationResult.data)}`); - } - console.log(); - } catch (error: any) { - console.log("⚠ get_device_location 测试失败(可能需要位置权限):", error.message, "\n"); - results.push({ tool: 'get_device_location', result: { success: false, message: error.message } }); - } - - // 5. 测试 list_installed_apps - console.log("5. 测试 list_installed_apps..."); - try { - const appsResult = await list_installed_apps({ include_system_apps: false }); - results.push({ tool: 'list_installed_apps', result: appsResult }); - console.log("✓ list_installed_apps 测试完成"); - if (appsResult.data) { - console.log(` 已安装应用数量: ${appsResult.data.length || 0}`); - } - console.log(); - } catch (error: any) { - console.log("⚠ list_installed_apps 测试失败(可能需要用户授权):", error.message, "\n"); - results.push({ tool: 'list_installed_apps', result: { success: false, message: error.message } }); - } - - // 6. 测试 get_system_setting(读取一个常见的系统设置) - console.log("6. 测试 get_system_setting..."); - try { - const settingResult = await get_system_setting({ setting: 'screen_brightness', namespace: 'system' }); - results.push({ tool: 'get_system_setting', result: settingResult }); - console.log("✓ get_system_setting 测试完成"); - if (settingResult.data) { - console.log(` screen_brightness = ${settingResult.data.value || settingResult.data}`); - } - console.log(); - } catch (error: any) { - console.log("⚠ get_system_setting 测试失败:", error.message, "\n"); - results.push({ tool: 'get_system_setting', result: { success: false, message: error.message } }); - } - - // 7-11. 其他需要用户授权或可能造成系统变更的工具,仅做说明不实际执行 - console.log("7-11. 跳过破坏性/需要特殊权限的工具测试:"); - console.log(" ⊘ modify_system_setting - 需要WRITE_SETTINGS权限,会修改系统设置"); - console.log(" ⊘ install_app - 需要INSTALL_PACKAGES权限"); - console.log(" ⊘ uninstall_app - 需要DELETE_PACKAGES权限"); - console.log(" ⊘ start_app - 需要应用包名,可能会启动应用"); - console.log(" ⊘ stop_app - 需要KILL_BACKGROUND_PROCESSES权限\n"); - - results.push({ tool: 'modify_system_setting', result: { success: null, message: '未测试(避免修改系统)' } }); - results.push({ tool: 'install_app', result: { success: null, message: '未测试(需要APK文件)' } }); - results.push({ tool: 'uninstall_app', result: { success: null, message: '未测试(避免卸载应用)' } }); - results.push({ tool: 'start_app', result: { success: null, message: '未测试(避免启动应用)' } }); - results.push({ tool: 'stop_app', result: { success: null, message: '未测试(避免停止应用)' } }); - - console.log("=== System Tools 测试完成 ===\n"); - console.log("测试结果汇总:"); - results.forEach((r, i) => { - const status = r.result.success === true ? '✓' : r.result.success === false ? '✗' : '⊘'; - console.log(`${i + 1}. ${status} ${r.tool}: ${r.result.message}`); - }); - - const successCount = results.filter(r => r.result.success === true).length; - const failCount = results.filter(r => r.result.success === false).length; - const skipCount = results.filter(r => r.result.success === null).length; - - console.log(`\n总计: ${successCount} 成功, ${failCount} 失败, ${skipCount} 跳过`); - - complete({ - success: true, - message: "系统工具全面测试完成", - data: { - results, - summary: { - total: results.length, - success: successCount, - failed: failCount, - skipped: skipCount - } - } - }); - } catch (error: any) { - console.error("测试过程中发生错误:", error); - complete({ - success: false, - message: `测试失败: ${error.message}`, - data: results - }); - } - } - - return { - get_system_setting: (params: { setting: string, namespace?: string }) => wrapToolExecution(get_system_setting, params), - modify_system_setting: (params: { setting: string, value: string, namespace?: string }) => wrapToolExecution(modify_system_setting, params), - install_app: (params: { path: string }) => wrapToolExecution(install_app, params), - uninstall_app: (params: { package_name: string, keep_data?: boolean }) => wrapToolExecution(uninstall_app, params), - list_installed_apps: (params: { include_system_apps?: boolean }) => wrapToolExecution(list_installed_apps, params), - start_app: (params: { package_name: string, activity?: string }) => wrapToolExecution(start_app, params), - stop_app: (params: { package_name: string }) => wrapToolExecution(stop_app, params), - send_broadcast: (params: { action: string, package_name?: string, component?: string, uri?: string, extras?: any }) => wrapToolExecution(send_broadcast, params), - execute_intent: (params: { type?: 'activity' | 'broadcast' | 'service' | string, action?: string, uri?: string, package_name?: string, component?: string, flags?: number | number[] | string, extras?: any }) => wrapToolExecution(execute_intent, params), - get_notifications: (params: { limit?: number, include_ongoing?: boolean }) => wrapToolExecution(get_notifications, params), - get_app_usage_time: (params: { package_name?: string, since_hours?: number, limit?: number, include_system_apps?: boolean }) => wrapToolExecution(get_app_usage_time, params), - get_device_location: (params: { high_accuracy?: boolean, timeout?: number }) => wrapToolExecution(get_device_location, params), - request_bluetooth_permission: (params: Record) => wrapToolExecution(request_bluetooth_permission, params), - get_bluetooth_state: (params: Record) => wrapToolExecution(get_bluetooth_state, params), - request_enable_bluetooth: (params: Record) => wrapToolExecution(request_enable_bluetooth, params), - list_bluetooth_bonded_devices: (params: Record) => wrapToolExecution(list_bluetooth_bonded_devices, params), - scan_bluetooth_devices: (params: BluetoothScanParams) => wrapToolExecution(scan_bluetooth_devices, params), - bluetooth_connect: (params: BluetoothConnectParams) => wrapToolExecution(bluetooth_connect, params), - bluetooth_listen: (params: BluetoothListenParams) => wrapToolExecution(bluetooth_listen, params), - bluetooth_accept: (params: BluetoothAcceptParams) => wrapToolExecution(bluetooth_accept, params), - bluetooth_send: (params: BluetoothPayloadParams) => wrapToolExecution(bluetooth_send, params), - bluetooth_read: (params: BluetoothReadParams) => wrapToolExecution(bluetooth_read, params), - bluetooth_send_and_read: (params: BluetoothSendAndReadParams) => wrapToolExecution(bluetooth_send_and_read, params), - bluetooth_close: (params: BluetoothCloseParams) => wrapToolExecution(bluetooth_close, params), - bluetooth_ble_connect: (params: BluetoothBleConnectParams) => wrapToolExecution(bluetooth_ble_connect, params), - bluetooth_ble_discover_services: (params: BluetoothBleDiscoverParams) => wrapToolExecution(bluetooth_ble_discover_services, params), - bluetooth_ble_read_characteristic: (params: BluetoothBleCharacteristicParams) => wrapToolExecution(bluetooth_ble_read_characteristic, params), - bluetooth_ble_write_characteristic: (params: BluetoothBleWriteParams) => wrapToolExecution(bluetooth_ble_write_characteristic, params), - bluetooth_ble_write_and_read_characteristic: (params: BluetoothBleWriteAndReadParams) => wrapToolExecution(bluetooth_ble_write_and_read_characteristic, params), - bluetooth_ble_subscribe_characteristic: (params: BluetoothBleSubscribeParams) => wrapToolExecution(bluetooth_ble_subscribe_characteristic, params), - bluetooth_ble_read_notifications: (params: BluetoothBleReadNotificationsParams) => wrapToolExecution(bluetooth_ble_read_notifications, params), - get_device_info: (params: {}) => wrapToolExecution(get_device_info, params), - main, - }; -})(); - -exports.get_system_setting = SystemTools.get_system_setting; -exports.modify_system_setting = SystemTools.modify_system_setting; -exports.install_app = SystemTools.install_app; -exports.uninstall_app = SystemTools.uninstall_app; -exports.list_installed_apps = SystemTools.list_installed_apps; -exports.start_app = SystemTools.start_app; -exports.stop_app = SystemTools.stop_app; -exports.send_broadcast = SystemTools.send_broadcast; -exports.execute_intent = SystemTools.execute_intent; -exports.get_notifications = SystemTools.get_notifications; -exports.get_app_usage_time = SystemTools.get_app_usage_time; -exports.get_device_location = SystemTools.get_device_location; -exports.request_bluetooth_permission = SystemTools.request_bluetooth_permission; -exports.get_bluetooth_state = SystemTools.get_bluetooth_state; -exports.request_enable_bluetooth = SystemTools.request_enable_bluetooth; -exports.list_bluetooth_bonded_devices = SystemTools.list_bluetooth_bonded_devices; -exports.scan_bluetooth_devices = SystemTools.scan_bluetooth_devices; -exports.bluetooth_connect = SystemTools.bluetooth_connect; -exports.bluetooth_listen = SystemTools.bluetooth_listen; -exports.bluetooth_accept = SystemTools.bluetooth_accept; -exports.bluetooth_send = SystemTools.bluetooth_send; -exports.bluetooth_read = SystemTools.bluetooth_read; -exports.bluetooth_send_and_read = SystemTools.bluetooth_send_and_read; -exports.bluetooth_close = SystemTools.bluetooth_close; -exports.bluetooth_ble_connect = SystemTools.bluetooth_ble_connect; -exports.bluetooth_ble_discover_services = SystemTools.bluetooth_ble_discover_services; -exports.bluetooth_ble_read_characteristic = SystemTools.bluetooth_ble_read_characteristic; -exports.bluetooth_ble_write_characteristic = SystemTools.bluetooth_ble_write_characteristic; -exports.bluetooth_ble_write_and_read_characteristic = SystemTools.bluetooth_ble_write_and_read_characteristic; -exports.bluetooth_ble_subscribe_characteristic = SystemTools.bluetooth_ble_subscribe_characteristic; -exports.bluetooth_ble_read_notifications = SystemTools.bluetooth_ble_read_notifications; -exports.get_device_info = SystemTools.get_device_info; -exports.main = SystemTools.main; diff --git a/scripts/tasker.ts b/scripts/tasker.ts deleted file mode 100644 index dc38001..0000000 --- a/scripts/tasker.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* METADATA -{ - "name": "tasker", - - "display_name": { - "zh": "Tasker 触发器", - "en": "Tasker Trigger" - }, - "description": { - "zh": "集成 Tasker 插件事件触发工具,通过本包可向 Tasker 发送事件。", - "en": "Integration for triggering Tasker plugin events. This package lets you send events to Tasker." - }, - "enabledByDefault": false, - "category": "Workflow", - "tools": [ - { - "name": "trigger_tasker_event", - "description": { - "zh": "触发一个 Tasker 事件。使用 task_type 指定事件类型,可传 arg1..arg5 或 args_json。", - "en": "Trigger a Tasker event. Use task_type to specify the event type. You can pass arg1..arg5 or args_json." - }, - "parameters": [ - { "name": "task_type", "description": { "zh": "事件类型标识", "en": "Event type identifier" }, "type": "string", "required": true }, - { "name": "arg1", "description": { "zh": "可选参数1", "en": "Optional argument 1" }, "type": "string", "required": false }, - { "name": "arg2", "description": { "zh": "可选参数2", "en": "Optional argument 2" }, "type": "string", "required": false }, - { "name": "arg3", "description": { "zh": "可选参数3", "en": "Optional argument 3" }, "type": "string", "required": false }, - { "name": "arg4", "description": { "zh": "可选参数4", "en": "Optional argument 4" }, "type": "string", "required": false }, - { "name": "arg5", "description": { "zh": "可选参数5", "en": "Optional argument 5" }, "type": "string", "required": false }, - { "name": "args_json", "description": { "zh": "以JSON形式传递任意参数", "en": "Pass arbitrary parameters as a JSON string" }, "type": "string", "required": false } - ] - } - ] -}*/ -/// -const TaskerIntegration = (function () { - async function trigger_tasker_event(params: Tasker.TriggerTaskerEventParams) { - const data: string = await Tools.Tasker.triggerEvent(params); - return { - success: true, - message: "Tasker 事件已触发", - data - }; - } - - async function wrapToolExecution(func: (params: any) => Promise, params?: any) { - try { - const result = await func(params || {}); - complete(result); - } catch (error: any) { - console.error(`Tool ${func.name} failed unexpectedly`, error); - complete({ success: false, message: String(error && error.message ? error.message : error) }); - } - } - - return { - trigger_tasker_event: (params?: Tasker.TriggerTaskerEventParams) => wrapToolExecution(trigger_tasker_event, params) - }; -})(); - -exports.trigger_tasker_event = TaskerIntegration.trigger_tasker_event; \ No newline at end of file diff --git a/scripts/tavily.ts b/scripts/tavily.ts deleted file mode 100644 index d8c2a58..0000000 --- a/scripts/tavily.ts +++ /dev/null @@ -1,198 +0,0 @@ -/* METADATA -{ - "name": "tavily", - - "display_name": { - "zh": "Tavily 搜索", - "en": "Tavily Search" - }, - "description": { - "zh": "使用Tavily API执行高级网络搜索、内容提取、网站爬取和站点地图生成。", - "en": "Use the Tavily API for advanced web search, content extraction, website crawling, and sitemap generation." - }, - "env": [ - "TAVILY_API_KEY" - ], - "category": "Search", - "tools": [ - { - "name": "search", - "description": { "zh": "一个强大的网络搜索工具,使用Tavily的AI搜索引擎提供全面、实时的结果。", "en": "Powerful web search using Tavily's AI search engine for comprehensive, up-to-date results." }, - "parameters": [ - { "name": "query", "description": { "zh": "搜索查询", "en": "Search query" }, "type": "string", "required": true }, - { "name": "search_depth", "description": { "zh": "搜索深度,可以是 'basic' 或 'advanced'", "en": "Search depth: 'basic' or 'advanced'" }, "type": "string", "required": false, "default": "basic" }, - { "name": "topic", "description": { "zh": "搜索类别,可以是 'general' 或 'news'", "en": "Search topic: 'general' or 'news'" }, "type": "string", "required": false, "default": "general" }, - { "name": "days", "description": { "zh": "包含在搜索结果中的过去天数", "en": "Number of past days to include in results" }, "type": "number", "required": false }, - { "name": "max_results", "description": { "zh": "返回的最大搜索结果数", "en": "Maximum number of search results" }, "type": "number", "required": false, "default": 10 }, - { "name": "include_images", "description": { "zh": "在响应中包含与查询相关的图像列表", "en": "Include a list of images related to the query in the response" }, "type": "boolean", "required": false, "default": false }, - { "name": "include_raw_content", "description": { "zh": "包含每个搜索结果的已清理和解析的HTML内容", "en": "Include cleaned and parsed HTML content for each search result" }, "type": "boolean", "required": false, "default": false }, - { "name": "include_domains", "description": { "zh": "要专门包含在搜索结果中的域名列表", "en": "Domain list to specifically include in results" }, "type": "array", "required": false, "default": [] }, - { "name": "exclude_domains", "description": { "zh": "要专门排除的域名列表", "en": "Domain list to specifically exclude from results" }, "type": "array", "required": false, "default": [] } - ] - }, - { - "name": "extract", - "description": { "zh": "一个强大的网页内容提取工具,从指定的URL检索和处理原始内容。", "en": "Extract web content from specified URLs and process the raw content." }, - "parameters": [ - { "name": "urls", "description": { "zh": "要提取内容的URL列表", "en": "List of URLs to extract content from" }, "type": "array", "required": true }, - { "name": "extract_depth", "description": { "zh": "提取深度 - 'basic' 或 'advanced'", "en": "Extraction depth: 'basic' or 'advanced'" }, "type": "string", "required": false, "default": "basic" }, - { "name": "include_images", "description": { "zh": "在响应中包含从URL中提取的图像列表", "en": "Include a list of images extracted from the URL(s)" }, "type": "boolean", "required": false, "default": false }, - { "name": "format", "description": { "zh": "提取内容的格式,'markdown' 或 'text'", "en": "Output format: 'markdown' or 'text'" }, "type": "string", "required": false, "default": "markdown" } - ] - }, - { - "name": "crawl", - "description": { "zh": "一个强大的网络爬虫,从指定的基URL开始结构化地爬取网站。", "en": "Structured website crawler starting from a base URL." }, - "parameters": [ - { "name": "url", "description": { "zh": "开始爬取的根URL", "en": "Root URL to start crawling from" }, "type": "string", "required": true }, - { "name": "max_depth", "description": { "zh": "爬取的最大深度", "en": "Maximum crawl depth" }, "type": "number", "required": false, "default": 1 }, - { "name": "max_breadth", "description": { "zh": "每层要跟随的最大链接数", "en": "Maximum number of links to follow per depth level" }, "type": "number", "required": false, "default": 20 }, - { "name": "limit", "description": { "zh": "爬虫将处理的总链接数", "en": "Total number of links the crawler will process" }, "type": "number", "required": false, "default": 50 }, - { "name": "instructions", "description": { "zh": "给爬虫的自然语言指令", "en": "Natural-language instructions for the crawler" }, "type": "string", "required": false }, - { "name": "allow_external", "description": { "zh": "是否允许跟随外部域名的链接", "en": "Whether to allow following links to external domains" }, "type": "boolean", "required": false, "default": false }, - { "name": "format", "description": { "zh": "提取内容的格式,'markdown' 或 'text'", "en": "Output format: 'markdown' or 'text'" }, "type": "string", "required": false, "default": "markdown" } - ] - }, - { - "name": "map", - "description": { "zh": "一个强大的网站地图工具,创建网站URL的结构化地图。", "en": "Generate a structured sitemap of website URLs." }, - "parameters": [ - { "name": "url", "description": { "zh": "开始映射的根URL", "en": "Root URL to start mapping from" }, "type": "string", "required": true }, - { "name": "max_depth", "description": { "zh": "映射的最大深度", "en": "Maximum mapping depth" }, "type": "number", "required": false, "default": 1 }, - { "name": "max_breadth", "description": { "zh": "每层要跟随的最大链接数", "en": "Maximum number of links to follow per depth level" }, "type": "number", "required": false, "default": 20 }, - { "name": "limit", "description": { "zh": "爬虫将处理的总链接数", "en": "Total number of links the crawler will process" }, "type": "number", "required": false, "default": 50 } - ] - } - ] -}*/ - -const tavily = (function () { - const client = OkHttp.newClient(); - const BASE_URLS = { - search: 'https://api.tavily.com/search', - extract: 'https://api.tavily.com/extract', - crawl: 'https://api.tavily.com/crawl', - map: 'https://api.tavily.com/map' - }; - - async function makeTavilyRequest(endpoint: string, params: any): Promise { - const apiKey = getEnv("TAVILY_API_KEY"); - if (!apiKey) { - throw new Error("Tavily API key is not set. Please configure it in the environment variables."); - } - - const requestBody = { ...params }; - - const headers = { - 'accept': 'application/json', - 'content-type': 'application/json', - 'Authorization': `Bearer ${apiKey}` - }; - - try { - const request = client.newRequest() - .url(endpoint) - .method('POST') - .headers(headers) - .body(JSON.stringify(requestBody), 'json'); - - const response = await request.build().execute(); - - if (!response.isSuccessful()) { - throw new Error(`Tavily API Error: ${response.statusCode} - ${response.content}`); - } - return JSON.parse(response.content); - } catch (error: any) { - console.error(`Tavily request failed: ${error.message}`); - throw error; - } - } - - function formatSearchResults(response: any): string { - const output: string[] = []; - if (response.answer) { - output.push(`Answer: ${response.answer}`); - } - output.push('Detailed Results:'); - response.results.forEach((result: any) => { - output.push(`\nTitle: ${result.title}`); - output.push(`URL: ${result.url}`); - output.push(`Content: ${result.content}`); - if (result.raw_content) { - output.push(`Raw Content: ${result.raw_content}`); - } - }); - return output.join('\n'); - } - - function formatCrawlResults(response: any): string { - const output: string[] = []; - output.push(`Crawl Results:`); - output.push(`Base URL: ${response.base_url}`); - output.push('\nCrawled Pages:'); - response.results.forEach((page: any, index: number) => { - output.push(`\n[${index + 1}] URL: ${page.url}`); - if (page.raw_content) { - const contentPreview = page.raw_content.length > 200 - ? page.raw_content.substring(0, 200) + "..." - : page.raw_content; - output.push(`Content: ${contentPreview}`); - } - }); - return output.join('\n'); - } - - function formatMapResults(response: any): string { - const output: string[] = []; - output.push(`Site Map Results:`); - output.push(`Base URL: ${response.base_url}`); - output.push('\nMapped Pages:'); - response.results.forEach((page: string, index: number) => { - output.push(`\n[${index + 1}] URL: ${page}`); - }); - return output.join('\n'); - } - - async function search(params: any) { - const response = await makeTavilyRequest(BASE_URLS.search, params); - return formatSearchResults(response); - } - - async function extract(params: any) { - const response = await makeTavilyRequest(BASE_URLS.extract, params); - // Extract uses the same format as search - return formatSearchResults(response); - } - - async function crawl(params: any) { - const response = await makeTavilyRequest(BASE_URLS.crawl, params); - return formatCrawlResults(response); - } - - async function map(params: any) { - const response = await makeTavilyRequest(BASE_URLS.map, params); - return formatMapResults(response); - } - - async function wrap(func: (params: any) => Promise, params: any, successMessage: string, failMessage: string) { - try { - const result = await func(params); - complete({ success: true, message: successMessage, data: result }); - } catch (error: any) { - console.error(`Function ${func.name} failed! Error: ${error.message}`); - complete({ success: false, message: `${failMessage}: ${error.message}`, error_stack: error.stack }); - } - } - - return { - search: (p: any) => wrap(search, p, '搜索成功', '搜索失败'), - extract: (p: any) => wrap(extract, p, '提取成功', '提取失败'), - crawl: (p: any) => wrap(crawl, p, '爬取成功', '爬取失败'), - map: (p: any) => wrap(map, p, '映射成功', '映射失败') - }; -})(); - -exports.search = tavily.search; -exports.extract = tavily.extract; -exports.crawl = tavily.crawl; -exports.map = tavily.map; \ No newline at end of file diff --git a/scripts/time.ts b/scripts/time.ts deleted file mode 100644 index 5fcaf2f..0000000 --- a/scripts/time.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* METADATA -{ - name: time - - display_name: { - zh: "时间" - en: "Time" - } - description: { - zh: "提供时间相关功能。实际上,激活本包的同时已经能够获取时间了。" - en: "Provides time-related utilities. In practice, current time is already available once this package is enabled." - } - enabledByDefault: true - category: "Utility" - tools: [ - { - name: get_time - description: { - zh: "获取当前时间。当使用此包时,AI已经自动获取了当前的时间信息。" - en: "Get the current time. When using this package, the AI may already have the current time context." - } - parameters: [] - }, - { - name: format_time - description: { - zh: "格式化时间。提供各种时间格式化选项。" - en: "Format time. Provides various time formatting options." - } - parameters: [] - } - ] -}*/ - -const timePackage = (function () { - async function get_time(): Promise { - const now = new Date(); - - return { - timestamp: now.getTime(), - iso: now.toISOString(), - local: now.toLocaleString(), - date: { - year: now.getFullYear(), - month: now.getMonth() + 1, - day: now.getDate(), - weekday: now.toLocaleDateString(undefined, { weekday: 'long' }) - }, - time: { - hours: now.getHours(), - minutes: now.getMinutes(), - seconds: now.getSeconds() - } - }; - } - - async function format_time(): Promise { - const now = new Date(); - - const pad = (n: number) => n.toString().padStart(2, '0'); - - const hours = now.getHours(); - const minutes = now.getMinutes(); - const seconds = now.getSeconds(); - - const time24h = `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`; - - const suffix = hours >= 12 ? 'PM' : 'AM'; - const hour12 = hours % 12 === 0 ? 12 : hours % 12; - const time12h = `${pad(hour12)}:${pad(minutes)}:${pad(seconds)} ${suffix}`; - - return { - iso: now.toISOString(), - date: `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`, - time24h, - time12h - }; - } - - return { - get_time, - format_time - }; -})(); - -// 逐个导出 -exports.get_time = timePackage.get_time; -exports.format_time = timePackage.format_time; \ No newline at end of file diff --git a/scripts/various_search.ts b/scripts/various_search.ts deleted file mode 100644 index f44e32f..0000000 --- a/scripts/various_search.ts +++ /dev/null @@ -1,1002 +0,0 @@ -/* METADATA -{ - name: various_search - - display_name: { - zh: "多平台搜索" - en: "Multi-Platform Search" - } - category: "Search" - description: { zh: "提供多平台搜索功能(含图片搜索),支持从必应、百度、搜狗、夸克等平台获取搜索结果。", en: "Multi-platform search tools (including image search) that fetch results from Bing, Baidu, Sogou, Quark, and more." } - enabledByDefault: true - - tools: [ - { - name: search_bing - description: { zh: "使用必应搜索引擎进行搜索", en: "Search using the Bing search engine." } - parameters: [ - { - name: query - description: { zh: "搜索查询关键词", en: "Search query keywords." } - type: string - required: true - }, - { - name: includeLinks - description: { zh: "是否在结果中包含可点击的链接列表,默认为false。如果为true,AI可以根据返回的链接序号进行深入访问。", en: "Whether to include a clickable link list in results (default: false). If true, the AI can follow links by index." } - type: boolean - required: false - } - ] - }, - { - name: search_baidu - description: { zh: "使用百度搜索引擎进行搜索", en: "Search using the Baidu search engine." } - parameters: [ - { - name: query - description: { zh: "搜索查询关键词", en: "Search query keywords." } - type: string - required: true - }, - { - name: page - description: { zh: "搜索结果页码,默认为1", en: "Result page number (default: 1)." } - type: string - required: false - }, - { - name: includeLinks - description: { zh: "是否在结果中包含可点击的链接列表,默认为false。如果为true,AI可以根据返回的链接序号进行深入访问。", en: "Whether to include a clickable link list in results (default: false). If true, the AI can follow links by index." } - type: boolean - required: false - } - ] - }, - { - name: search_sogou - description: { zh: "使用搜狗搜索引擎进行搜索", en: "Search using the Sogou search engine." } - parameters: [ - { - name: query - description: { zh: "搜索查询关键词", en: "Search query keywords." } - type: string - required: true - }, - { - name: page - description: { zh: "搜索结果页码,默认为1", en: "Result page number (default: 1)." } - type: string - required: false - }, - { - name: includeLinks - description: { zh: "是否在结果中包含可点击的链接列表,默认为false。如果为true,AI可以根据返回的链接序号进行深入访问。", en: "Whether to include a clickable link list in results (default: false). If true, the AI can follow links by index." } - type: boolean - required: false - } - ] - }, - { - name: search_quark - description: { zh: "使用夸克搜索引擎进行搜索", en: "Search using the Quark search engine." } - parameters: [ - { - name: query - description: { zh: "搜索查询关键词", en: "Search query keywords." } - type: string - required: true - }, - { - name: page - description: { zh: "搜索结果页码,默认为1", en: "Result page number (default: 1)." } - type: string - required: false - }, - { - name: includeLinks - description: { zh: "是否在结果中包含可点击的链接列表,默认为false。如果为true,AI可以根据返回的链接序号进行深入访问。", en: "Whether to include a clickable link list in results (default: false). If true, the AI can follow links by index." } - type: boolean - required: false - } - ] - }, - { - name: combined_search - description: { zh: "在多个平台同时执行搜索。建议用户要求搜索的时候默认使用这个工具。", en: "Run searches across multiple platforms. Use this tool by default when the user asks to search." } - parameters: [ - { - name: query - description: { zh: "搜索查询关键词", en: "Search query keywords." } - type: string - required: true - }, - { - name: platforms - description: { zh: "搜索平台列表字符串,可选值包括\"bing\",\"baidu\",\"sogou\",\"quark\",多个平台用逗号分隔,比如\"bing,baidu,sogou,quark\"", en: "Comma-separated platform list. Supported: \"bing\", \"baidu\", \"sogou\", \"quark\". Example: \"bing,baidu,sogou,quark\"." } - type: string - required: true - }, - { - name: includeLinks - description: { zh: "是否在结果中包含可点击的链接列表,默认为false。聚合搜索时建议保持为false以节省输出,仅在需要深入访问时对单个搜索引擎使用。", en: "Whether to include a clickable link list in results (default: false). For combined search, keep it false to reduce output; enable it for a single engine when you need to open links." } - type: boolean - required: false - } - ] - }, - { - name: search - description: { zh: "兼容工具名:等同于 combined_search。用于处理模型误调用 search 的情况。", en: "Compatibility alias: equivalent to combined_search. Helps when models call search by mistake." } - parameters: [ - { - name: query - description: { zh: "搜索查询关键词", en: "Search query keywords." } - type: string - required: true - }, - { - name: platforms - description: { zh: "可选平台列表,默认 bing,baidu,sogou,quark", en: "Optional platform list, default bing,baidu,sogou,quark." } - type: string - required: false - }, - { - name: includeLinks - description: { zh: "是否返回链接列表,默认 false", en: "Whether to include links in result, default false." } - type: boolean - required: false - } - ] - }, - { - name: search_web - description: { zh: "兼容工具名:网页搜索别名。默认复用 combined_search 的统一搜索逻辑。", en: "Compatibility alias for web search. Reuses the unified combined_search flow." } - parameters: [ - { - name: query - description: { zh: "搜索查询关键词", en: "Search query keywords." } - type: string - required: true - }, - { - name: platforms - description: { zh: "可选平台列表,默认 bing,baidu,sogou,quark", en: "Optional platform list, default bing,baidu,sogou,quark." } - type: string - required: false - }, - { - name: includeLinks - description: { zh: "是否返回链接列表,默认 false", en: "Whether to include links in result, default false." } - type: boolean - required: false - } - ] - }, - { - name: search_bing_images - description: { zh: "使用必应图片搜索引擎进行图片搜索。返回内容会包含 visitKey 和 Images 编号;下载图片请用 download_file 的 visit_key + image_number(不要用 link_number 乱点页面链接)。", en: "Search images using Bing Images. The result includes visitKey and indexed Images; download images via download_file with visit_key + image_number (do not follow random page links via link_number)." } - parameters: [ - { - name: query - description: { zh: "搜索关键词", en: "Search query keywords." } - type: string - required: true - } - ] - }, - { - name: search_wikimedia_images - description: { zh: "使用 Wikimedia Commons 进行图片搜索(公共资源)。返回 visitKey + Images 编号;下载图片用 download_file(visit_key + image_number)。", en: "Search images using Wikimedia Commons (public domain/commons). Use visitKey + image_number with download_file to download images." } - parameters: [ - { - name: query - description: { zh: "搜索关键词", en: "Search query keywords." } - type: string - required: true - } - ] - }, - { - name: search_duckduckgo_images - description: { zh: "使用 DuckDuckGo Images 进行图片搜索。返回 visitKey + Images 编号;下载图片用 download_file(visit_key + image_number)。", en: "Search images using DuckDuckGo Images. Use visitKey + image_number with download_file to download images." } - parameters: [ - { - name: query - description: { zh: "搜索关键词", en: "Search query keywords." } - type: string - required: true - } - ] - }, - { - name: search_ecosia_images - description: { zh: "使用 Ecosia Images 进行图片搜索。返回 visitKey + Images 编号;下载图片用 download_file(visit_key + image_number)。", en: "Search images using Ecosia Images. Use visitKey + image_number with download_file to download images." } - parameters: [ - { - name: query - description: { zh: "搜索关键词", en: "Search query keywords." } - type: string - required: true - } - ] - }, - { - name: search_pexels_images - description: { zh: "使用 Pexels 进行图片搜索(高质量图库)。返回 visitKey + Images 编号;下载图片请用 download_file 的 visit_key + image_number。", en: "Search images using Pexels (high-quality stock). Use visitKey + image_number with download_file to download images." } - parameters: [ - { - name: query - description: { zh: "搜索关键词", en: "Search query keywords." } - type: string - required: true - } - ] - }, - { - name: search_pixabay_images - description: { zh: "使用 Pixabay 进行图片搜索(图库)。返回 visitKey + Images 编号;下载图片请用 download_file 的 visit_key + image_number。", en: "Search images using Pixabay (stock). Use visitKey + image_number with download_file to download images." } - parameters: [ - { - name: query - description: { zh: "搜索关键词", en: "Search query keywords." } - type: string - required: true - } - ] - } - ] -}*/ - -const various_search = (function () { - function normalizeText(input: string | undefined | null): string { - if (!input) return ""; - return String(input).toLowerCase().replace(/[\s\-_.,;:!?()[\]{}"'`~@#$%^&*+=|\\/<>]+/g, ""); - } - - const OUTBOUND_PATH_HINTS = [ - "/link", - "/url", - "/redirect", - "/jump", - "/out", - "/outlink", - "/ulink", - "/aladdin.php" - ]; - - const OUTBOUND_QUERY_KEYS = [ - "url", - "u", - "target", - "targeturl", - "target_url", - "to", - "dest", - "destination", - "redirect", - "redirect_url" - ]; - const MULTI_PART_TLDS = [ - "ac.uk", - "co.jp", - "co.uk", - "com.au", - "com.cn", - "com.hk", - "gov.cn", - "net.au", - "net.cn", - "org.au", - "org.cn", - "org.uk" - ]; - - type LinkTargetType = "invalid" | "landing" | "internal" | "wrapper" | "external"; - - function normalizePath(pathname: string | undefined | null): string { - const normalized = String(pathname || "").replace(/\/+$/g, ""); - return normalized || "/"; - } - - function parseUrlParts( - rawUrl: string | undefined | null, - baseUrl: string - ): { href: string; protocol: string; host: string; hostname: string; pathname: string; search: string } | null { - const rawText = String(rawUrl || "").trim(); - if (!rawText) { - return null; - } - let absoluteText = rawText; - if (absoluteText.startsWith("//")) { - absoluteText = `https:${absoluteText}`; - } else if (!/^[a-z]+:\/\//i.test(absoluteText)) { - if (!absoluteText.startsWith("/")) { - return null; - } - const base = baseUrl ? parseUrlParts(baseUrl, "") : null; - if (!base) { - return null; - } - absoluteText = `${base.protocol}//${base.host}${absoluteText}`; - } - const match = absoluteText.match(/^(https?):\/\/([^\/?#]+)([^?#]*)(\?[^#]*)?(?:#.*)?$/i); - if (!match) { - return null; - } - const protocol = `${match[1].toLowerCase()}:`; - const host = match[2].toLowerCase(); - const hostname = host.replace(/:\d+$/, ""); - const pathname = normalizePath(match[3] || "/"); - const search = match[4] || ""; - return { - href: `${protocol}//${host}${pathname}${search}`, - protocol, - host, - hostname, - pathname, - search - }; - } - - function normalizeUrl(rawUrl: string | undefined | null, baseUrl: string): string { - const parsed = parseUrlParts(rawUrl, baseUrl); - return parsed ? parsed.href : ""; - } - - function parseSearchEntries(searchText: string | undefined | null): Array<[string, string]> { - const search = String(searchText || "").replace(/^\?/, ""); - if (!search) { - return []; - } - return search - .split("&") - .map((item) => item.trim()) - .filter(Boolean) - .map((item) => { - const equalIndex = item.indexOf("="); - if (equalIndex === -1) { - return [item, ""]; - } - return [item.slice(0, equalIndex), item.slice(equalIndex + 1)]; - }); - } - - function getSiteDomain(hostname: string | undefined | null): string { - const parts = String(hostname || "").toLowerCase().split(".").filter(Boolean); - if (parts.length <= 2) { - return String(hostname || "").toLowerCase(); - } - const lastTwo = parts.slice(-2).join("."); - if (MULTI_PART_TLDS.includes(lastTwo) && parts.length >= 3) { - return parts.slice(-3).join("."); - } - return lastTwo; - } - - function countPathSegments(pathname: string | undefined | null): number { - return normalizePath(pathname).split("/").filter(Boolean).length; - } - - function getSortedSearch(searchText: string | undefined | null): string { - return parseSearchEntries(searchText) - .sort((left, right) => { - if (left[0] === right[0]) { - return left[1].localeCompare(right[1]); - } - return left[0].localeCompare(right[0]); - }) - .map(([key, value]) => `${key}=${value}`) - .join("&"); - } - - function toComparableUrl(rawUrl: string | undefined | null): string { - if (!rawUrl) return ""; - const parsed = parseUrlParts(rawUrl, ""); - if (!parsed) { - return String(rawUrl).trim(); - } - const search = getSortedSearch(parsed.search); - return `${parsed.protocol}//${parsed.host}${parsed.pathname}${search ? `?${search}` : ""}`; - } - - function getLinkText(link: any): string { - const text = link && (link.text || link.title || link.name); - return text ? String(text).replace(/\s+/g, " ").trim() : ""; - } - - function looksLikeUiText(text: string): boolean { - const raw = String(text || "").replace(/\s+/g, " ").trim(); - const normalized = normalizeText(text); - if (!normalized) return true; - if (/^\d+$/.test(normalized)) return true; - if (!/[a-z0-9\u4e00-\u9fa5]/i.test(raw)) return true; - if (normalized.length === 1) return true; - if (normalized.length === 2 && /^[a-z0-9]+$/i.test(normalized)) return true; - return false; - } - - function looksLikeSearchLandingUrl(rawUrl: string | undefined | null, sourceUrl: string): boolean { - if (!rawUrl || !sourceUrl) return false; - const candidate = parseUrlParts(rawUrl, sourceUrl); - const source = parseUrlParts(sourceUrl, ""); - if (!candidate || !source) return false; - if (toComparableUrl(candidate.href) === toComparableUrl(source.href)) { - return true; - } - if (candidate.host !== source.host) { - return false; - } - const candidateSearch = getSortedSearch(candidate.search); - const sourceSearch = getSortedSearch(source.search); - if (candidate.pathname === "/" && !candidateSearch) { - return true; - } - return candidate.pathname === source.pathname && (!candidateSearch || candidateSearch === sourceSearch); - } - - function looksLikeOutboundWrapper(rawUrl: string, sourceUrl: string): boolean { - const candidate = parseUrlParts(rawUrl, sourceUrl); - const source = parseUrlParts(sourceUrl, ""); - if (!candidate || !source || candidate.host !== source.host) { - return false; - } - const path = candidate.pathname.toLowerCase(); - if (OUTBOUND_PATH_HINTS.some((hint) => path === hint || path.endsWith(hint))) { - return true; - } - const search = candidate.search.replace(/^\?/, ""); - return OUTBOUND_QUERY_KEYS.some((key) => new RegExp(`(?:^|&)${key}(?:=|&|$)`, "i").test(search)); - } - - function looksLikeSameSiteSectionUrl(rawUrl: string, sourceUrl: string): boolean { - const candidate = parseUrlParts(rawUrl, sourceUrl); - const source = parseUrlParts(sourceUrl, ""); - if (!candidate || !source) { - return false; - } - if (candidate.host === source.host) { - return false; - } - if (getSiteDomain(candidate.hostname) !== getSiteDomain(source.hostname)) { - return false; - } - if (looksLikeOutboundWrapper(candidate.href, source.href)) { - return false; - } - return countPathSegments(candidate.pathname) <= 1; - } - - function classifyLinkTarget(rawUrl: string | undefined | null, sourceUrl: string): LinkTargetType { - const text = String(rawUrl || "").trim(); - if (!text) { - return "invalid"; - } - const lowered = text.toLowerCase(); - if (lowered.startsWith("javascript:") || lowered.startsWith("#")) { - return "invalid"; - } - const candidate = parseUrlParts(rawUrl, sourceUrl); - const source = parseUrlParts(sourceUrl, ""); - if (!candidate || !source) { - return "invalid"; - } - if (looksLikeSearchLandingUrl(candidate.href, source.href)) { - return "landing"; - } - if (looksLikeOutboundWrapper(candidate.href, source.href)) { - return "wrapper"; - } - if (candidate.host === source.host) { - return "internal"; - } - if (looksLikeSameSiteSectionUrl(candidate.href, source.href)) { - return "internal"; - } - if (getSiteDomain(candidate.hostname) === getSiteDomain(source.hostname)) { - return "internal"; - } - return "external"; - } - - function looksLikeInternalNavigationUrl(rawUrl: string | undefined | null, sourceUrl: string): boolean { - const type = classifyLinkTarget(rawUrl, sourceUrl); - return type === "invalid" || type === "landing" || type === "internal"; - } - - function extractUrlsFromText(rawText: string | undefined | null): string[] { - if (!rawText) return []; - const text = String(rawText); - const matches = text.match(/https?:\/\/[^\s)\]>"']+/g) || []; - const unique: string[] = []; - for (const url of matches) { - if (!unique.includes(url)) unique.push(url); - } - return unique; - } - - function extractBestUrlFromText(rawText: string | undefined | null, sourceUrl: string): string { - const urls = extractUrlsFromText(rawText); - const meaningful = urls.find((item) => { - const type = classifyLinkTarget(item, sourceUrl); - return type === "external" || type === "wrapper"; - }); - return meaningful || urls[0] || ""; - } - - function stripLeadingLinkDump(rawContent: string | undefined | null): string { - const lines = String(rawContent || "").split(/\r?\n/); - let index = 0; - while (index < lines.length && !lines[index].trim()) { - index++; - } - let dumpedLinkCount = 0; - while (index < lines.length && /^\[\d+\]\s/.test(lines[index].trim())) { - dumpedLinkCount++; - index++; - } - if (dumpedLinkCount === 0) { - return String(rawContent || ""); - } - while (index < lines.length && !lines[index].trim()) { - index++; - } - return lines.slice(index).join('\n'); - } - - function collectLinkCandidates(link: any, sourceUrl: string): string[] { - const values = [ - link?.realUrl, - link?.real_url, - link?.targetUrl, - link?.target_url, - link?.originUrl, - link?.origin_url, - link?.href, - link?.url, - link?.target, - link?.rawUrl, - link?.link - ]; - const unique: string[] = []; - for (const value of values) { - const normalized = normalizeUrl(value ? String(value).trim() : "", sourceUrl); - if (normalized && !unique.includes(normalized)) { - unique.push(normalized); - } - } - return unique; - } - - function pickBestLinkUrl(link: any, sourceUrl: string): string { - const candidates = collectLinkCandidates(link, sourceUrl); - const meaningful = candidates.find((item) => { - const type = classifyLinkTarget(item, sourceUrl); - return type === "external" || type === "wrapper"; - }); - if (meaningful) return meaningful; - - const fromText = extractBestUrlFromText(getLinkText(link), sourceUrl); - if (fromText) return fromText; - - return candidates[0] || ""; - } - - function scoreStructuralCandidate(text: string, url: string, sourceUrl: string, relativeIndex: number): number { - let score = 0; - if (!text || looksLikeUiText(text)) { - score -= 6; - } else { - score += 2; - } - const type = classifyLinkTarget(url, sourceUrl); - if (type === "external") { - score += 5; - } else if (type === "wrapper") { - score += 4; - } else if (type === "landing" || type === "internal") { - score -= 8; - } else { - score -= 4; - } - score -= relativeIndex * 0.15; - return score; - } - - function findResultBlockStart(links: any[], sourceUrl: string, scanLimit: number = 48, windowSize: number = 6, minCandidates: number = 3): number { - const upperBound = Math.min(links.length, scanLimit); - for (let start = 0; start < upperBound; start++) { - let candidateCount = 0; - const hosts: string[] = []; - for (let offset = 0; offset < windowSize && start + offset < upperBound; offset++) { - const link = links[start + offset]; - const text = getLinkText(link); - const url = pickBestLinkUrl(link, sourceUrl); - if (!text || looksLikeUiText(text)) { - continue; - } - const type = classifyLinkTarget(url, sourceUrl); - if (type !== "external" && type !== "wrapper") { - continue; - } - candidateCount++; - const parsed = parseUrlParts(url, sourceUrl); - const host = parsed ? parsed.hostname : ""; - if (host && !hosts.includes(host)) { - hosts.push(host); - } - } - if (candidateCount >= minCandidates && hosts.length >= 2) { - return start; - } - } - return 0; - } - - function isClusteredResultLink(links: any[], sourceUrl: string, targetIndex: number, radius: number = 3, minCandidates: number = 3): boolean { - let candidateCount = 0; - const hosts: string[] = []; - const start = Math.max(0, targetIndex - radius); - const end = Math.min(links.length - 1, targetIndex + radius); - for (let index = start; index <= end; index++) { - const link = links[index]; - const text = getLinkText(link); - const url = pickBestLinkUrl(link, sourceUrl); - if (!text || looksLikeUiText(text)) { - continue; - } - const type = classifyLinkTarget(url, sourceUrl); - if (type !== "external" && type !== "wrapper") { - continue; - } - candidateCount++; - const parsed = parseUrlParts(url, sourceUrl); - const host = parsed ? parsed.hostname : ""; - if (host && !hosts.includes(host)) { - hosts.push(host); - } - } - return candidateCount >= minCandidates && hosts.length >= 2; - } - - function buildProbeIndexes(links: any[], sourceUrl: string, maxCount: number = 8, probeWindow: number = 24): number[] { - const resultStart = findResultBlockStart(links, sourceUrl); - return links - .slice(resultStart, resultStart + probeWindow) - .map((link, relativeIndex) => { - const index = resultStart + relativeIndex; - const text = getLinkText(link); - const url = pickBestLinkUrl(link, sourceUrl); - return { - index, - score: scoreStructuralCandidate(text, url, sourceUrl, relativeIndex) - }; - }) - .filter((item) => item.score > -4 && isClusteredResultLink(links, sourceUrl, item.index)) - .slice(0, maxCount) - .map((item) => item.index); - } - - async function resolveLinkUrlsByVisitKey( - response: any, - sourceUrl: string, - maxCount: number = 8 - ): Promise> { - if (!response || !response.visitKey || !response.links || !Array.isArray(response.links)) { - return {}; - } - const resolved: Record = {}; - const indexes = buildProbeIndexes(response.links, sourceUrl, maxCount); - const tasks = indexes.map(async (index) => { - try { - const follow = await Tools.Net.visit({ - visit_key: response.visitKey, - link_number: index + 1 - }); - const followUrl = follow && follow.url ? normalizeUrl(String(follow.url), sourceUrl) : ""; - const followContent = follow && follow.content ? String(follow.content) : ""; - const contentUrl = extractBestUrlFromText(followContent, sourceUrl); - resolved[index] = { - url: chooseBestUrl("", followUrl, sourceUrl) || chooseBestUrl("", contentUrl, sourceUrl) - }; - } catch (error: any) { - console.error(`[resolveLinkUrlsByVisitKey] link ${index + 1} failed: ${error.message}`); - resolved[index] = { url: "" }; - } - }); - await Promise.all(tasks); - return resolved; - } - - function chooseBestUrl(directUrl: string, resolvedUrl: string, sourceUrl: string): string { - const candidates = [resolvedUrl, directUrl].filter(Boolean); - for (const candidate of candidates) { - if (classifyLinkTarget(candidate, sourceUrl) === "external") { - return normalizeUrl(candidate, sourceUrl); - } - } - return ""; - } - - function shouldKeepLink(links: any[], index: number, text: string, url: string, sourceUrl: string): boolean { - if (!url) return false; - if (!text || looksLikeUiText(text)) return false; - if (classifyLinkTarget(url, sourceUrl) !== "external") { - return false; - } - return isClusteredResultLink(links, sourceUrl, index); - } - - async function buildLinkLines(response: any, sourceUrl: string, maxItems: number = 20): Promise { - const resultStart = findResultBlockStart(response.links, sourceUrl); - const resolvedByIndex = await resolveLinkUrlsByVisitKey(response, sourceUrl); - const lines: string[] = []; - const seen = new Set(); - for (let index = resultStart; index < response.links.length; index++) { - const link = response.links[index]; - const text = getLinkText(link); - const directUrl = pickBestLinkUrl(link, sourceUrl); - const resolved = resolvedByIndex[index]; - const bestUrl = chooseBestUrl(directUrl, resolved && resolved.url ? resolved.url : "", sourceUrl); - if (!shouldKeepLink(response.links, index, text, bestUrl, sourceUrl)) { - continue; - } - const key = toComparableUrl(bestUrl); - if (!key || seen.has(key)) { - continue; - } - seen.add(key); - lines.push(`[${index + 1}] ${text} - ${bestUrl}`); - if (lines.length >= maxItems) { - break; - } - } - return lines; - } - - async function performSearch(platform: string, url: string, includeLinks: boolean = false) { - try { - const response = await Tools.Net.visit({ url }); - if (!response) { - throw new Error(`无法获取 ${platform} 搜索结果`); - } - - let parts: string[] = []; - let hasFilteredLinks = false; - if (response.visitKey !== undefined) { - parts.push(String(response.visitKey)); - } - if (includeLinks && response.links && Array.isArray(response.links) && response.links.length > 0) { - const linksLines = await buildLinkLines(response, url); - if (linksLines.length > 0) { - parts.push(linksLines.join('\n')); - hasFilteredLinks = true; - } - } else if (includeLinks && response.content) { - const extractedUrls = extractUrlsFromText(response.content); - if (extractedUrls.length > 0) { - const lines = extractedUrls.slice(0, 20).map((item, index) => `[${index + 1}] ${item}`); - parts.push(lines.join('\n')); - } - } - if (response.content !== undefined && (!includeLinks || !hasFilteredLinks)) { - const contentText = includeLinks && response.links && Array.isArray(response.links) - ? stripLeadingLinkDump(response.content) - : String(response.content); - parts.push(String(contentText)); - } - - return { - platform, - content: parts.join('\n') - }; - } catch (error: any) { - return { - platform, - content: `${platform} 搜索失败: ${error.message}` - }; - } - } - - async function performImageSearch(platform: string, url: string) { - try { - const response = await Tools.Net.visit({ url, include_image_links: true }); - if (!response) { - throw new Error(`无法获取 ${platform} 图片搜索结果`); - } - - let parts: string[] = []; - - if (response.visitKey !== undefined) { - parts.push(String(response.visitKey)); - } - - if (response.imageLinks && Array.isArray(response.imageLinks) && response.imageLinks.length > 0) { - const maxItems = 20; - const imagesLines = response.imageLinks.slice(0, maxItems).map((link: string, index: number) => { - const lastSeg = String(link).split('/').pop() || 'image'; - const name = lastSeg.split('?')[0] || 'image'; - return `[${index + 1}] ${name}`; - }); - parts.push("Images:"); - parts.push(imagesLines.join('\n')); - } - - if (response.content !== undefined) { - parts.push(String(response.content)); - } - - return { - platform, - content: parts.join('\n') - }; - } catch (error: any) { - return { - platform, - content: `${platform} 图片搜索失败: ${error.message}` - }; - } - } - - async function search_bing(query: string, includeLinks: boolean = false) { - const encodedQuery = encodeURIComponent(query); - const url = `https://cn.bing.com/search?q=${encodedQuery}&FORM=HDRSC1`; - return performSearch('bing', url, includeLinks); - } - - async function search_baidu(query: string, pageStr?: string, includeLinks: boolean = false) { - let page = 1; - if (pageStr) { - page = parseInt(pageStr, 10); - } - const pn = (page - 1) * 10; - const encodedQuery = encodeURIComponent(query); - const url = `https://www.baidu.com/s?wd=${encodedQuery}&pn=${pn}`; - return performSearch('baidu', url, includeLinks); - } - - async function search_sogou(query: string, pageStr?: string, includeLinks: boolean = false) { - let page = 1; - if (pageStr) { - page = parseInt(pageStr, 10); - } - const encodedQuery = encodeURIComponent(query); - const url = `https://www.sogou.com/web?query=${encodedQuery}&page=${page}`; - return performSearch('sogou', url, includeLinks); - } - - async function search_quark(query: string, pageStr?: string, includeLinks: boolean = false) { - let page = 1; - if (pageStr) { - page = parseInt(pageStr, 10); - } - const encodedQuery = encodeURIComponent(query); - const url = `https://quark.sm.cn/s?q=${encodedQuery}&page=${page}`; - return performSearch('quark', url, includeLinks); - } - - async function search_bing_images(query: string) { - const encodedQuery = encodeURIComponent(query); - const url = `https://www.bing.com/images/search?q=${encodedQuery}`; - return performImageSearch('bing_images', url); - } - - async function search_wikimedia_images(query: string) { - const encodedQuery = encodeURIComponent(query); - const url = `https://commons.wikimedia.org/wiki/Special:MediaSearch?type=image&search=${encodedQuery}`; - return performImageSearch('wikimedia_images', url); - } - - async function search_duckduckgo_images(query: string) { - const encodedQuery = encodeURIComponent(query); - const url = `https://duckduckgo.com/?q=${encodedQuery}&iax=images&ia=images`; - return performImageSearch('duckduckgo_images', url); - } - - async function search_ecosia_images(query: string) { - const encodedQuery = encodeURIComponent(query); - const url = `https://www.ecosia.org/images?q=${encodedQuery}`; - return performImageSearch('ecosia_images', url); - } - - async function search_pexels_images(query: string) { - const encodedQuery = encodeURIComponent(query); - const url = `https://www.pexels.com/search/${encodedQuery}/`; - return performImageSearch('pexels_images', url); - } - - async function search_pixabay_images(query: string) { - const encodedQuery = encodeURIComponent(query); - const url = `https://pixabay.com/images/search/${encodedQuery}/`; - return performImageSearch('pixabay_images', url); - } - - const searchFunctions: any = { - bing: search_bing, - baidu: search_baidu, - sogou: search_sogou, - quark: search_quark - }; - - async function combined_search(query: string, platforms: string, includeLinks: boolean = true) { - const platformKeysRaw = platforms.split(','); - const platformKeys: string[] = []; - for (const platform of platformKeysRaw) { - const trimmedPlatform = platform.trim(); - if (trimmedPlatform) { - platformKeys.push(trimmedPlatform); - } - } - - const searchPromises: Promise[] = []; - for (const platform of platformKeys) { - const searchFn = searchFunctions[platform]; - if (searchFn) { - if (platform === 'bing') { - searchPromises.push(searchFn(query, includeLinks)); - } else { - // 注意:这里我们假设组合搜索总是从第一页开始 - searchPromises.push(searchFn(query, '1', includeLinks)); - } - } else { - searchPromises.push(Promise.resolve({ platform, success: false, message: `不支持的搜索平台: ${platform}` })); - } - } - - return Promise.all(searchPromises); - } - - async function search(query: string, platforms?: string, includeLinks: boolean = false) { - return combined_search(query, platforms || "bing,baidu,sogou,quark", includeLinks); - } - - async function search_web(query: string, platforms?: string, includeLinks: boolean = false) { - return search(query, platforms, includeLinks); - } - - async function main() { - const result = await combined_search('如何学习编程', 'bing,baidu,sogou,quark'); - console.log(JSON.stringify(result, null, 2)); - } - - function wrap(coreFunction: (...args: any[]) => Promise, parameterNames: string[]) { - return async (params: any) => { - const args = parameterNames.map((name) => params[name]); - return coreFunction(...args); - }; - } - - return { - search_bing, - search_baidu, - search_sogou, - search_quark, - search, - search_web, - search_bing_images, - search_wikimedia_images, - search_duckduckgo_images, - search_ecosia_images, - search_pexels_images, - search_pixabay_images, - combined_search, - wrap, - main - }; -})(); - -exports.search_bing = various_search.wrap(various_search.search_bing, ['query', 'includeLinks']); -exports.search_baidu = various_search.wrap(various_search.search_baidu, ['query', 'page', 'includeLinks']); -exports.search_sogou = various_search.wrap(various_search.search_sogou, ['query', 'page', 'includeLinks']); -exports.search_quark = various_search.wrap(various_search.search_quark, ['query', 'page', 'includeLinks']); -exports.search = various_search.wrap(various_search.search, ['query', 'platforms', 'includeLinks']); -exports.search_web = various_search.wrap(various_search.search_web, ['query', 'platforms', 'includeLinks']); -exports.search_bing_images = various_search.wrap(various_search.search_bing_images, ['query']); -exports.search_wikimedia_images = various_search.wrap(various_search.search_wikimedia_images, ['query']); -exports.search_duckduckgo_images = various_search.wrap(various_search.search_duckduckgo_images, ['query']); -exports.search_ecosia_images = various_search.wrap(various_search.search_ecosia_images, ['query']); -exports.search_pexels_images = various_search.wrap(various_search.search_pexels_images, ['query']); -exports.search_pixabay_images = various_search.wrap(various_search.search_pixabay_images, ['query']); -exports.combined_search = various_search.wrap(various_search.combined_search, ['query', 'platforms', 'includeLinks']); - -exports.main = various_search.main; diff --git a/scripts/vflow_trigger.ts b/scripts/vflow_trigger.ts deleted file mode 100644 index e4fb2e2..0000000 --- a/scripts/vflow_trigger.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* METADATA -{ - name: "vflow_trigger" - display_name: { - zh: "VFlow 触发器" - en: "VFlow Trigger" - } - description: { - zh: "触发vflow app的工作流。" - en: "Trigger VFlow app workflows." - } - enabledByDefault: false - category: "Workflow" - tools: [ - { - name: "trigger_vflow_workflow" - description: { - zh: "根据 workflow_id 触发 VFlow 工作流(需安装 com.chaomixian.vflow)。" - en: "Trigger a VFlow workflow by workflow_id (requires com.chaomixian.vflow installed)." - } - parameters: [ - { name: "workflow_id", description: { zh: "工作流 ID", en: "Workflow ID" }, type: "string", required: true } - ] - } - ] -}*/ - -/// - -const VFlowTrigger = (function () { - const DEFAULT_ACTION = "com.chaomixian.vflow.EXECUTE_WORKFLOW_SHORTCUT"; - const DEFAULT_COMPONENT = "com.chaomixian.vflow/.ui.common.ShortcutExecutorActivity"; - - interface TriggerParams { - workflow_id: string; - } - - async function trigger_vflow_workflow(params: TriggerParams): Promise { - if (!params || !params.workflow_id) { - return { success: false, message: "workflow_id 不能为空" }; - } - - const result = await Tools.System.intent({ - type: "activity", - action: DEFAULT_ACTION, - component: DEFAULT_COMPONENT, - extras: { - workflow_id: params.workflow_id - } - }); - - return { - success: true, - message: "已触发 VFlow 工作流", - data: result - }; - } - - async function wrapToolExecution

(func: (params: P) => Promise, params: P): Promise { - try { - const result = await func(params); - complete(result); - } catch (error: any) { - console.error(`Tool ${func.name} failed unexpectedly`, error); - complete({ success: false, message: String(error && error.message ? error.message : error) }); - } - } - - return { - trigger_vflow_workflow: (params: TriggerParams) => wrapToolExecution(trigger_vflow_workflow, params) - }; -})(); - -exports.trigger_vflow_workflow = VFlowTrigger.trigger_vflow_workflow; diff --git a/scripts/workflow.ts b/scripts/workflow.ts deleted file mode 100644 index 79eeb44..0000000 --- a/scripts/workflow.ts +++ /dev/null @@ -1,565 +0,0 @@ -/* METADATA -{ - name: "workflow" - - display_name: { - zh: "工作流管理" - en: "Workflow Management" - } - description: { - zh: '''工作流管理工具:创建/查询/更新/启用/禁用/删除/触发执行;支持 on_success/on_error 分支;支持语音触发(speech)。''' - en: '''Workflow management tools for creating/querying/updating/enabling/disabling/deleting workflows, triggering execution, and branching via on_success/on_error. Supports speech trigger (speech).''' - } - "category": "Workflow", - enabledByDefault: true - - tools: [ - { - name: "usage_advice" - description: { - zh: ''' -工作流工具使用建议(给 AI): - -- 核心概念(请优先对齐这些语义): - - 节点类型:trigger/execute/condition/logic/extract - - 触发节点类型:manual/schedule/tasker/intent/speech - - 参数引用(ParameterValue):静态值 vs 引用其他节点输出 - - 分支连线 condition 的语义:on_success/on_error/true/false/regex - -- 重要:create_workflow/update_workflow 的 nodes/connections 参数底层类型是 string(JSON 数组字符串)。 - - 本示例封装允许你直接传对象数组,封装层会自动 JSON.stringify。 - -- 推荐流程: - 1) 先 get_all_workflows 找到候选 workflow_id。 - 2) 再 get_workflow 获取 nodes/connections 全量结构。 -- 3) 如果你要“整体替换” nodes/connections:构造完整的新数组后用 update_workflow 一次性覆盖。 -- 4) 如果你只想“增量修改” nodes/connections:优先使用 patch_workflow(node_patches/connection_patches)。 - -- 节点与连线的 ID: - - 节点 id 可省略(服务端会生成),但如果你要创建 connections,强烈建议你在 nodes 里显式写好 id。 - - connections 里 source/target 可以用: - - sourceNodeId/targetNodeId(推荐) - - 或 source/target/from/to - - 或 sourceIndex/targetIndex(按 nodes 数组下标) - - 或 sourceNodeName/targetNodeName(不推荐:同名会歧义) - -- 分支连线 condition(核心): - - 通用关键字(适用于任何节点类型): - - condition = "on_success" | "success" | "ok":源节点成功时触发 - - condition = "on_error" | "error" | "failed":源节点失败时触发(失败分支/补救逻辑) - - 对 ConditionNode / LogicNode: - - condition 为空:默认代表 true 分支(相当于 "true") - - condition = "false":false 分支 - - condition = 其它字符串:当作 Regex 匹配源节点输出字符串 - - 对非 Condition/Logic 节点: - - condition 为空:等价于 on_success(表示“源节点执行成功就走”) - - condition = on_error:表示“源节点失败就走”(失败分支) - -- 参数引用(ParameterValue): - - 静态值:直接写字符串/数字/布尔值即可(会被当作 StaticValue) - - 引用某节点输出:写对象 { nodeId: "" } - - 兼容字段:nodeId / ref / refNodeId - -- 触发节点类型(TriggerNode.triggerType): - - manual:手动触发(UI 点“触发工作流”) - - schedule:定时触发(由 WorkManager 调度) - - tasker:Tasker 事件触发 - - intent:系统广播 Intent 触发 - - speech:语音识别事件触发(当识别文本命中正则时触发;可多工作流同时触发) - - 触发配置 TriggerNode.triggerConfig(注意:值全是 string): - - schedule: - - schedule_type: interval | specific_time | cron - - interval_ms: "900000" (15分钟) - - specific_time: "2026-01-04 10:30" (格式依实现) - - cron_expression: "15 * * * *" (简化 cron) - - repeat: "true"/"false" - - enabled: "true"/"false" - - tasker: - - command: "start_meeting" (当 Tasker params 中包含该字符串则触发) - - intent: - - action: "com.example.MY_ACTION" (当收到该 action 的 Intent 则触发) - - speech: - - pattern: ".*(打开|启动).*(对话|聊天|悬浮窗).*" (正则;匹配识别文本) - - ignore_case: "true"/"false" (可选,默认 true) - - require_final: "true"/"false" (可选,默认 true;true 表示仅 final 结果触发) - - cooldown_ms: "3000" (可选,默认 3000;每个节点的触发冷却) -''' - en: ''' -Workflow tool usage advice (for the AI): - -- Core concepts (align your reasoning with these semantics): - - Node types: trigger/execute/condition/logic/extract - - Trigger node types: manual/schedule/tasker/intent/speech - - Parameter references (ParameterValue): static values vs references to another node output - - Connection "condition" meaning: on_success/on_error/true/false/regex - -- Important: in create_workflow/update_workflow, nodes/connections are strings (JSON array strings) at the API layer. - - This example wrapper lets you pass object arrays directly; it will JSON.stringify automatically. - -- Recommended flow: - 1) Call get_all_workflows to find the candidate workflow_id. - 2) Call get_workflow to retrieve the full nodes/connections structure. -- 3) If you want to replace nodes/connections entirely: build the full new arrays and use update_workflow once to overwrite. -- 4) If you only want incremental changes: prefer patch_workflow (node_patches/connection_patches). - -- Node and connection IDs: - - Node id can be omitted (server will generate it), but if you need to create connections, strongly recommend explicitly setting node ids in nodes. - - In connections, source/target can be provided as: - - sourceNodeId/targetNodeId (recommended) - - or source/target/from/to - - or sourceIndex/targetIndex (index within nodes array) - - or sourceNodeName/targetNodeName (not recommended: duplicates are ambiguous) - -- Connection condition (core): - - Global keywords (works for any node type): - - condition = "on_success" | "success" | "ok": trigger when the source node succeeds - - condition = "on_error" | "error" | "failed": trigger when the source node fails (error branch / recovery) - - For ConditionNode / LogicNode: - - empty condition: defaults to true branch (equivalent to "true") - - condition = "false": false branch - - other string: treated as a Regex to match the source node output string - - For non-Condition/Logic nodes: - - empty condition: equivalent to on_success (proceed if the source node succeeded) - - condition = on_error: proceed if the source node failed (error branch) - -- Parameter references (ParameterValue): - - Static value: write a string/number/boolean directly (treated as StaticValue) - - Reference another node output: write an object { nodeId: "" } - - compatible fields: nodeId / ref / refNodeId - -- Trigger node types (TriggerNode.triggerType): - - manual: manual trigger (tap "trigger workflow" in UI) - - schedule: scheduled trigger (WorkManager) - - tasker: triggered by Tasker events - - intent: triggered by Android broadcast intents - - speech: triggered by speech recognition events (fires when recognized text matches a regex; multiple workflows can match) - - Trigger configuration TriggerNode.triggerConfig (note: all values are strings): - - schedule: - - schedule_type: interval | specific_time | cron - - interval_ms: "900000" (15 minutes) - - specific_time: "2026-01-04 10:30" (format depends on implementation) - - cron_expression: "15 * * * *" (simplified cron) - - repeat: "true"/"false" - - enabled: "true"/"false" - - tasker: - - command: "start_meeting" (triggered when Tasker params contains this string) - - intent: - - action: "com.example.MY_ACTION" (triggered when receiving this action) - - speech: - - pattern: ".*(open|start).*(chat|floating).*" (regex; matches recognized text) - - ignore_case: "true"/"false" (optional, default true) - - require_final: "true"/"false" (optional, default true; if true, only final results trigger) - - cooldown_ms: "3000" (optional, default 3000; per-node cooldown) -''' - } - parameters: [] - } - - { - name: "get_all_workflows" - description: { zh: "获取所有工作流列表(只含概要信息:ID/名称/启用/统计等)。", en: "List all workflows (summary only: id/name/enabled/stats, etc.)." } - parameters: [] - } - - { - name: "get_workflow" - description: { zh: "获取指定工作流完整详情(nodes + connections)。", en: "Get full details of a specific workflow (nodes + connections)." } - parameters: [ - { name: "workflow_id", description: { zh: "工作流 ID", en: "Workflow ID" }, type: "string", required: true } - ] - } - - { - name: "create_workflow" - description: { - zh: ''' -创建工作流。 - -参数说明: -- nodes: JSON 数组字符串(推荐传对象数组,让封装自动 stringify) -- connections: JSON 数组字符串(同上) - -节点类型(node.type): -- trigger / execute / condition / logic / extract - -Execute 节点: -- actionType: 工具名(如 "visit_web" / "list_files" / "get_system_setting" ...) -- actionConfig: 工具参数对象,支持 ParameterValue(静态值/节点引用) - -Condition 节点: -- left/right: ParameterValue -- operator: EQ/NE/GT/GTE/LT/LTE/CONTAINS/NOT_CONTAINS/IN/NOT_IN - -Logic 节点: -- operator: AND/OR - -Extract(运算)节点: -- 说明:node.type 仍然是 "extract"(兼容旧数据),但语义更接近“运算器/计算节点”。 -- source: ParameterValue(部分模式不需要,例如 RANDOM_INT) -- mode: - - REGEX:正则提取 - - expression: 正则表达式 - - group/defaultValue 可选 - - JSON:JSON 路径提取 - - expression: JSON 路径(简化实现) - - defaultValue 可选 - - SUB:字符串截取 - - startIndex: 起始下标 - - length: 长度(-1 表示到结尾) - - defaultValue 可选 - - CONCAT:字符串拼接 - - others: ParameterValue[](被拼接项列表) - - RANDOM_INT:随机整数 - - randomMin/randomMax - - useFixed: 是否使用固定值(可选) - - fixedValue: 固定整数(可选,仅 useFixed=true 时生效;输出仍为数字字符串) - - RANDOM_STRING:随机字符串 - - randomStringLength: 长度 - - randomStringCharset: 字符集(可选,默认字母数字) - - useFixed: 是否使用固定值(可选) - - fixedValue: 固定字符串(可选,仅 useFixed=true 时生效) -''' - en: ''' -Create a workflow. - -Parameter notes: -- nodes: JSON array string (recommended: pass object arrays and let the wrapper stringify) -- connections: JSON array string (same as above) - -Node types (node.type): -- trigger / execute / condition / logic / extract - -Execute node: -- actionType: tool name (e.g. "visit_web" / "list_files" / "get_system_setting" ...) -- actionConfig: tool parameter object, supports ParameterValue (static value / node reference) - -Condition node: -- left/right: ParameterValue -- operator: EQ/NE/GT/GTE/LT/LTE/CONTAINS/NOT_CONTAINS/IN/NOT_IN - -Logic node: -- operator: AND/OR - -Extract (operator) node: -- Note: node.type is still "extract" for backward compatibility, but it behaves like an "operator" node. -- source: ParameterValue (not required for some modes like RANDOM_INT) -- mode: - - REGEX: regex extraction - - expression: regex pattern - - group/defaultValue are optional - - JSON: JSON path extraction - - expression: JSON path (simplified) - - defaultValue optional - - SUB: substring - - startIndex - - length (-1 means to end) - - defaultValue optional - - CONCAT: string concatenation - - others: ParameterValue[] - - RANDOM_INT: random integer - - randomMin/randomMax - - useFixed: whether to use a fixed value (optional) - - fixedValue: fixed integer (optional, only effective when useFixed=true; output is still a numeric string) - - RANDOM_STRING: random string - - randomStringLength - - randomStringCharset (optional, default: alphanumeric) - - useFixed: whether to use a fixed value (optional) - - fixedValue: fixed string (optional, only effective when useFixed=true) -''' - } - parameters: [ - { name: "name", description: { zh: "工作流名称", en: "Workflow name" }, type: "string", required: true } - { name: "description", description: { zh: "工作流描述(可选)", en: "Workflow description (optional)" }, type: "string", required: false } - { name: "nodes", description: { zh: "可选,节点 JSON 数组字符串(或直接传节点数组,由封装 stringify)", en: "Optional. Nodes JSON array string (or pass an array and the wrapper will stringify)." }, type: "string", required: false } - { name: "connections", description: { zh: "可选,连线 JSON 数组字符串(或直接传连线数组,由封装 stringify)", en: "Optional. Connections JSON array string (or pass an array and the wrapper will stringify)." }, type: "string", required: false } - { name: "enabled", description: { zh: "可选,是否启用(默认 true)", en: "Optional. Whether to enable (default: true)." }, type: "boolean", required: false } - ] - } - - { - name: "update_workflow" - description: { - zh: ''' -更新工作流。 - -注意:update_workflow 的 nodes / connections 是“整体覆盖”。 -- 若你只改其中一部分,推荐使用 patch_workflow。 -- 或者:get_workflow 取回旧结构 -> 本地构造新数组(保留未改部分)-> update_workflow 一次性传回。 -''' - en: ''' -Update a workflow. - -Note: nodes/connections in update_workflow are full overwrites. -- If you only change part of them, prefer patch_workflow. -- Or: call get_workflow to fetch the old structure -> build new arrays locally (keeping unchanged parts) -> call update_workflow once. -''' - } - parameters: [ - { name: "workflow_id", description: { zh: "工作流 ID", en: "Workflow ID" }, type: "string", required: true } - { name: "name", description: { zh: "可选,新名称", en: "Optional. New name." }, type: "string", required: false } - { name: "description", description: { zh: "可选,新描述", en: "Optional. New description." }, type: "string", required: false } - { name: "nodes", description: { zh: "可选,节点 JSON 数组字符串(整体覆盖)", en: "Optional. Nodes JSON array string (full overwrite)." }, type: "string", required: false } - { name: "connections", description: { zh: "可选,连线 JSON 数组字符串(整体覆盖)", en: "Optional. Connections JSON array string (full overwrite)." }, type: "string", required: false } - { name: "enabled", description: { zh: "可选,是否启用", en: "Optional. Whether to enable." }, type: "boolean", required: false } - ] - } - - { - name: "patch_workflow" - description: { - zh: ''' -差异更新工作流(增量 patch)。 - -使用 node_patches / connection_patches 传入 JSON 数组字符串: -- op: add | update | remove -- id: 可选 -- node / connection: 对象 - -说明: -- add:必须提供 node/connection -- update:必须提供 id 或 node.id/connection.id -- remove:必须提供 id -''' - en: ''' -Patch a workflow (incremental update). - -Use node_patches / connection_patches as JSON array strings: -- op: add | update | remove -- id: optional -- node / connection: object - -Notes: -- add: must provide node/connection -- update: must provide id OR node.id/connection.id -- remove: must provide id -''' - } - parameters: [ - { name: "workflow_id", description: { zh: "工作流 ID", en: "Workflow ID" }, type: "string", required: true } - { name: "name", description: { zh: "可选,新名称", en: "Optional. New name." }, type: "string", required: false } - { name: "description", description: { zh: "可选,新描述", en: "Optional. New description." }, type: "string", required: false } - { name: "enabled", description: { zh: "可选,是否启用", en: "Optional. Whether to enable." }, type: "boolean", required: false } - { name: "node_patches", description: { zh: "可选,节点 patch JSON 数组字符串", en: "Optional. Node patch JSON array string." }, type: "string", required: false } - { name: "connection_patches", description: { zh: "可选,连线 patch JSON 数组字符串", en: "Optional. Connection patch JSON array string." }, type: "string", required: false } - ] - } - - { - name: "enable_workflow" - description: { zh: "启用指定工作流。", en: "Enable a specific workflow." } - parameters: [ - { name: "workflow_id", description: { zh: "工作流 ID", en: "Workflow ID" }, type: "string", required: true } - ] - } - - { - name: "disable_workflow" - description: { zh: "禁用指定工作流。", en: "Disable a specific workflow." } - parameters: [ - { name: "workflow_id", description: { zh: "工作流 ID", en: "Workflow ID" }, type: "string", required: true } - ] - } - - { - name: "delete_workflow" - description: { zh: "删除指定工作流。", en: "Delete a specific workflow." } - parameters: [ - { name: "workflow_id", description: { zh: "工作流 ID", en: "Workflow ID" }, type: "string", required: true } - ] - } - - { - name: "trigger_workflow" - description: { zh: "触发指定工作流执行(相当于 UI 手动触发)。", en: "Trigger execution of a workflow (equivalent to manual trigger in UI)." } - parameters: [ - { name: "workflow_id", description: { zh: "工作流 ID", en: "Workflow ID" }, type: "string", required: true } - ] - } - ] -}*/ - -/// - -const WorkflowIntegration = (function () { - async function usage_advice(_params: {}): Promise { - return { - success: true, - message: "请阅读 workflow 工具 METADATA 中的 usage_advice 说明:包含节点/连线 schema、分支 condition 规则、触发类型与配置示例。建议在脚本里直接使用 Tools.Workflow.getAll/get/create/update/patch/setEnabled/enable/disable/delete/trigger 等封装方法。" - }; - } - - /** - * 获取所有工作流 - * @returns 工作流列表 - */ - async function get_all_workflows(): Promise { - const data = await Tools.Workflow.getAll(); - return { - success: true, - message: "成功获取工作流列表", - data - }; - } - - /** - * 创建新工作流 - * @param params 工作流参数 - * @returns 创建结果 - */ - async function create_workflow(params: Workflow.CreateParams): Promise { - const data = await Tools.Workflow.create( - params.name, - params.description || "", - params.nodes || null, - params.connections || null, - params.enabled - ); - return { - success: true, - message: "成功创建工作流", - data - }; - } - - /** - * 获取工作流详情 - * @param params 获取参数 - * @returns 工作流详情 - */ - async function get_workflow(params: Workflow.GetParams): Promise { - const data = await Tools.Workflow.get(params.workflow_id); - return { - success: true, - message: "成功获取工作流详情", - data - }; - } - - /** - * 更新工作流 - * @param params 更新参数 - * @returns 更新结果 - */ - async function update_workflow(params: Workflow.UpdateParams): Promise { - const { workflow_id, ...updates } = params; - const data = await Tools.Workflow.update(workflow_id, updates); - return { - success: true, - message: "成功更新工作流", - data - }; - } - - /** - * 差异更新工作流(增量 patch) - * @param params patch 参数 - * @returns 更新结果 - */ - async function patch_workflow(params: Workflow.PatchParams): Promise { - const { workflow_id, ...patch } = params; - const data = await Tools.Workflow.patch(workflow_id, patch); - return { - success: true, - message: "成功差异更新工作流", - data - }; - } - - /** - * 启用工作流 - * @param params 启用参数 - * @returns 更新结果 - */ - async function enable_workflow(params: Workflow.EnableParams): Promise { - const data = await Tools.Workflow.enable(params.workflow_id); - return { - success: true, - message: "成功启用工作流", - data - }; - } - - /** - * 禁用工作流 - * @param params 禁用参数 - * @returns 更新结果 - */ - async function disable_workflow(params: Workflow.DisableParams): Promise { - const data = await Tools.Workflow.disable(params.workflow_id); - return { - success: true, - message: "成功禁用工作流", - data - }; - } - - /** - * 删除工作流 - * @param params 删除参数 - * @returns 删除结果 - */ - async function delete_workflow(params: Workflow.DeleteParams): Promise { - const data = await Tools.Workflow['delete'](params.workflow_id); - return { - success: true, - message: "成功删除工作流", - data - }; - } - - /** - * 触发工作流执行 - * @param params 触发参数 - * @returns 执行结果 - */ - async function trigger_workflow(params: Workflow.TriggerParams): Promise { - const data = await Tools.Workflow.trigger(params.workflow_id); - return { - success: true, - message: "成功触发工作流", - data - }; - } - - /** - * 包装工具执行,处理错误和结果 - * @param func 要执行的函数 - * @param params 函数参数 - */ - async function wrapToolExecution

(func: (params: P) => Promise, params: P): Promise { - try { - const result = await func(params); - complete(result); - } catch (error: any) { - console.error(`Tool ${func.name} failed unexpectedly`, error); - complete({ success: false, message: String(error && error.message ? error.message : error) }); - } - } - - return { - usage_advice: (params: {}) => wrapToolExecution(usage_advice, params), - get_all_workflows: () => wrapToolExecution(get_all_workflows, {} as any), - create_workflow: (params: Workflow.CreateParams) => wrapToolExecution(create_workflow, params), - get_workflow: (params: Workflow.GetParams) => wrapToolExecution(get_workflow, params), - update_workflow: (params: Workflow.UpdateParams) => wrapToolExecution(update_workflow, params), - patch_workflow: (params: Workflow.PatchParams) => wrapToolExecution(patch_workflow, params), - enable_workflow: (params: Workflow.EnableParams) => wrapToolExecution(enable_workflow, params), - disable_workflow: (params: Workflow.DisableParams) => wrapToolExecution(disable_workflow, params), - delete_workflow: (params: Workflow.DeleteParams) => wrapToolExecution(delete_workflow, params), - trigger_workflow: (params: Workflow.TriggerParams) => wrapToolExecution(trigger_workflow, params) - }; -})(); - -exports.usage_advice = WorkflowIntegration.usage_advice; -exports.get_all_workflows = WorkflowIntegration.get_all_workflows; -exports.create_workflow = WorkflowIntegration.create_workflow; -exports.get_workflow = WorkflowIntegration.get_workflow; -exports.update_workflow = WorkflowIntegration.update_workflow; -exports.patch_workflow = WorkflowIntegration.patch_workflow; -exports.enable_workflow = WorkflowIntegration.enable_workflow; -exports.disable_workflow = WorkflowIntegration.disable_workflow; -exports.delete_workflow = WorkflowIntegration.delete_workflow; -exports.trigger_workflow = WorkflowIntegration.trigger_workflow; - diff --git a/scripts/xai_draw.ts b/scripts/xai_draw.ts deleted file mode 100644 index 5ea9015..0000000 --- a/scripts/xai_draw.ts +++ /dev/null @@ -1,707 +0,0 @@ -/* METADATA -{ - "name": "xai_draw", - "display_name": { - "zh": "xAI 图片与视频", - "en": "xAI Images and Video" - }, - "description": { - "zh": "使用 xAI 官方接口生成图片和视频,并保存到本地。", - "en": "Generate images and videos with the official xAI APIs and save them locally." - }, - "env": [ - { - "name": "XAI_API_KEY", - "description": { - "zh": "xAI API Key(必填)", - "en": "xAI API key (required)" - }, - "required": true - }, - { - "name": "XAI_IMAGE_MODEL", - "description": { - "zh": "默认图片模型(可选;未传 model 时使用,默认 grok-2-image-1212)", - "en": "Default image model (optional; used when model is omitted, default grok-2-image-1212)" - }, - "required": false - }, - { - "name": "XAI_VIDEO_MODEL", - "description": { - "zh": "默认视频模型(可选;未传 model 时使用,默认 grok-imagine-video)", - "en": "Default video model (optional; used when model is omitted, default grok-imagine-video)" - }, - "required": false - } - ], - "category": "Draw", - "tools": [ - { - "name": "draw_image", - "description": { - "zh": "根据提示词调用 xAI 图像生成 API 生成图片,保存到本地并返回 Markdown 图片提示。", - "en": "Generate an image via the xAI image generation API using a prompt, save it locally, and return a Markdown image reference." - }, - "parameters": [ - { "name": "prompt", "description": { "zh": "绘图提示词(英文或中文皆可)", "en": "Prompt for image generation (Chinese or English)" }, "type": "string", "required": true }, - { "name": "model", "description": { "zh": "xAI 图像模型名称;不传则优先取 XAI_IMAGE_MODEL,再用默认值 grok-2-image-1212", "en": "xAI image model name; falls back to XAI_IMAGE_MODEL, then grok-2-image-1212" }, "type": "string", "required": false }, - { "name": "size", "description": { "zh": "图片尺寸,例如 1024x1024(可选)", "en": "Image size, e.g. 1024x1024 (optional)" }, "type": "string", "required": false }, - { "name": "file_name", "description": { "zh": "自定义保存到本地的文件名(不含路径和扩展名)", "en": "Custom output file name (without path or extension)" }, "type": "string", "required": false } - ] - }, - { - "name": "draw_video", - "description": { - "zh": "根据提示词调用 xAI 官方视频生成 API 生成视频,支持文生视频、图生视频和视频编辑,轮询完成后下载到本地并返回本地视频链接提示。", - "en": "Generate a video with the official xAI video API. Supports text-to-video, image-to-video, and video editing. Polls until completion, downloads locally, and returns local video link hints." - }, - "parameters": [ - { "name": "prompt", "description": { "zh": "视频提示词", "en": "Video prompt" }, "type": "string", "required": true }, - { "name": "model", "description": { "zh": "视频模型;不传则优先取 XAI_VIDEO_MODEL,再用默认值 grok-imagine-video", "en": "Video model; falls back to XAI_VIDEO_MODEL, then grok-imagine-video" }, "type": "string", "required": false }, - { "name": "aspect_ratio", "description": { "zh": "输出比例,可选 1:1、16:9、9:16、4:3、3:4、3:2、2:3;默认 16:9;视频编辑模式不支持", "en": "Output aspect ratio. Supported: 1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3. Defaults to 16:9; not supported for video editing." }, "type": "string", "required": false }, - { "name": "resolution", "description": { "zh": "输出分辨率,仅支持 480p 或 720p;默认 480p;视频编辑模式不支持", "en": "Output resolution, only 480p or 720p. Defaults to 480p; not supported for video editing." }, "type": "string", "required": false }, - { "name": "duration", "description": { "zh": "输出时长,支持 1-15 秒;默认 5;视频编辑模式不支持", "en": "Output duration from 1 to 15 seconds. Defaults to 5; not supported for video editing." }, "type": "number", "required": false }, - { "name": "image_url", "description": { "zh": "图生视频输入图 URL(可选)", "en": "Input image URL for image-to-video (optional)" }, "type": "string", "required": false }, - { "name": "image_path", "description": { "zh": "图生视频输入图本地路径(可选,会转成 data URL)", "en": "Local input image path for image-to-video (optional; converted to a data URL)" }, "type": "string", "required": false }, - { "name": "video_url", "description": { "zh": "视频编辑输入视频 URL(可选)", "en": "Input video URL for video editing (optional)" }, "type": "string", "required": false }, - { "name": "file_name", "description": { "zh": "自定义保存到本地的文件名(不含路径和扩展名)", "en": "Custom output file name (without path or extension)" }, "type": "string", "required": false }, - { "name": "poll_interval_ms", "description": { "zh": "轮询间隔毫秒数,默认 5000", "en": "Polling interval in milliseconds, default 5000" }, "type": "number", "required": false }, - { "name": "max_wait_time_ms", "description": { "zh": "最大等待毫秒数,默认 600000", "en": "Maximum wait time in milliseconds, default 600000" }, "type": "number", "required": false } - ] - } - ] -}*/ -/// - -const xaiDraw = (function () { - const HTTP_TIMEOUT_MS = 600000; - const client = OkHttp.newBuilder() - .connectTimeout(HTTP_TIMEOUT_MS) - .readTimeout(HTTP_TIMEOUT_MS) - .writeTimeout(HTTP_TIMEOUT_MS) - .build(); - - const DEFAULT_IMAGE_MODEL = "grok-2-image-1212"; - const DEFAULT_VIDEO_MODEL = "grok-imagine-video"; - const DEFAULT_POLL_INTERVAL_MS = 5000; - const DEFAULT_MAX_WAIT_TIME_MS = 600000; - const DEFAULT_VIDEO_ASPECT_RATIO = "16:9"; - const DEFAULT_VIDEO_RESOLUTION = "480p"; - const DEFAULT_VIDEO_DURATION = 5; - const VIDEO_ASPECT_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3"]; - const VIDEO_RESOLUTIONS = ["480p", "720p"]; - const MIN_VIDEO_DURATION = 1; - const MAX_VIDEO_DURATION = 15; - - const API_BASE_URL = "https://api.x.ai/v1"; - const IMAGE_API_ENDPOINT = `${API_BASE_URL}/images/generations`; - const VIDEO_GENERATION_ENDPOINT = `${API_BASE_URL}/videos/generations`; - - const DRAW_ROOT_DIR = getPluginConfigDir("draw"); - const STORAGE_DIR = `${DRAW_ROOT_DIR}/xai_draw`; - const DRAWS_DIR = `${STORAGE_DIR}/draws`; - const VIDEOS_DIR = `${STORAGE_DIR}/videos`; - - type JsonMap = Record; - - type VideoParams = { - prompt: string; - model?: string; - aspect_ratio?: string; - resolution?: string; - duration?: number | string; - image_url?: string; - image_path?: string; - video_url?: string; - file_name?: string; - poll_interval_ms?: number | string; - max_wait_time_ms?: number | string; - }; - - function getErrorMessage(error: unknown): string { - if (error instanceof Error) return error.message; - return String(error); - } - - function getErrorStack(error: unknown): string | undefined { - if (error instanceof Error) return error.stack; - return undefined; - } - - function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); - } - - function getApiKey(): string { - const apiKey = getEnv("XAI_API_KEY"); - if (!apiKey) { - throw new Error("XAI_API_KEY 未配置,请在环境变量中设置 xAI 的 API Key。"); - } - return apiKey; - } - - function getDefaultImageModel(): string { - const fromEnv = String(getEnv("XAI_IMAGE_MODEL") || "").trim(); - return fromEnv || DEFAULT_IMAGE_MODEL; - } - - function getDefaultVideoModel(): string { - const fromEnv = String(getEnv("XAI_VIDEO_MODEL") || "").trim(); - return fromEnv || DEFAULT_VIDEO_MODEL; - } - - function sanitizeFileName(name: string, fallbackPrefix: string): string { - const safe = String(name || "").replace(/[\\/:*?"<>|]/g, "_").trim(); - if (!safe) { - return `${fallbackPrefix}_${Date.now()}`; - } - return safe.substring(0, 80); - } - - function buildFileName(prompt: string, customName: string | null | undefined, fallbackPrefix: string): string { - if (customName && customName.trim().length > 0) { - return sanitizeFileName(customName, fallbackPrefix); - } - const shortPrompt = prompt.length > 40 ? `${prompt.substring(0, 40)}...` : prompt; - const base = sanitizeFileName(shortPrompt || fallbackPrefix, fallbackPrefix); - return `${base}_${Date.now()}`; - } - - function guessExtensionFromUrl(url: string, fallback: string): string { - const match = String(url || "").match(/\.(png|jpg|jpeg|webp|gif|mp4|mov|webm|mkv)(?:\?|#|$)/i); - if (match && match[1]) { - return match[1].toLowerCase(); - } - return fallback; - } - - function guessMimeTypeFromPath(path: string): string { - const normalized = String(path || "").trim().toLowerCase(); - if (normalized.endsWith(".png")) return "image/png"; - if (normalized.endsWith(".webp")) return "image/webp"; - if (normalized.endsWith(".gif")) return "image/gif"; - if (normalized.endsWith(".jpg") || normalized.endsWith(".jpeg")) return "image/jpeg"; - return "application/octet-stream"; - } - - function isProbablyUrl(value: string): boolean { - return /^https?:\/\//i.test(String(value || "").trim()); - } - - function normalizePositiveInteger(value: unknown, fallback: number): number { - if (value === undefined || value === null || value === "") return fallback; - const parsed = typeof value === "number" ? value : parseInt(String(value), 10); - if (!Number.isFinite(parsed) || parsed <= 0) return fallback; - return Math.floor(parsed); - } - - function normalizeVideoAspectRatio(value: unknown): string { - const raw = String(value || "").trim(); - if (!raw) return DEFAULT_VIDEO_ASPECT_RATIO; - if (!VIDEO_ASPECT_RATIOS.includes(raw)) { - throw new Error(`aspect_ratio 仅支持 ${VIDEO_ASPECT_RATIOS.join(" 或 ")}。`); - } - return raw; - } - - function normalizeVideoResolution(value: unknown): string { - const raw = String(value || "").trim().toLowerCase(); - if (!raw) return DEFAULT_VIDEO_RESOLUTION; - if (!VIDEO_RESOLUTIONS.includes(raw)) { - throw new Error(`resolution 仅支持 ${VIDEO_RESOLUTIONS.join(" 或 ")}。`); - } - return raw; - } - - function normalizeVideoDuration(value: unknown): number { - if (value === undefined || value === null || value === "") return DEFAULT_VIDEO_DURATION; - const parsed = typeof value === "number" ? value : parseInt(String(value), 10); - if (!Number.isFinite(parsed)) { - throw new Error("duration 必须是数字。"); - } - const normalized = Math.floor(parsed); - if (normalized < MIN_VIDEO_DURATION || normalized > MAX_VIDEO_DURATION) { - throw new Error(`duration 仅支持 ${MIN_VIDEO_DURATION}-${MAX_VIDEO_DURATION} 秒。`); - } - return normalized; - } - - async function ensureDirectories(): Promise { - const dirs = [DRAW_ROOT_DIR, STORAGE_DIR, DRAWS_DIR, VIDEOS_DIR]; - for (const dir of dirs) { - try { - const result = await Tools.Files.mkdir(dir); - if (!result.successful) { - console.warn(`创建目录失败(可能已存在): ${dir} -> ${result.details}`); - } - } catch (error) { - console.warn(`创建目录异常: ${dir} -> ${getErrorMessage(error)}`); - } - } - } - - async function parseJsonResponse(response: { content: string }, label: string): Promise { - try { - const parsed = JSON.parse(response.content) as unknown; - if (!isRecord(parsed)) { - throw new Error("响应不是对象"); - } - return parsed as JsonMap; - } catch (error) { - throw new Error(`解析 ${label} 响应失败: ${getErrorMessage(error)}`); - } - } - - function extractApiErrorMessage(payload: JsonMap): string { - const directError = payload.error; - if (typeof directError === "string" && directError.trim()) { - return directError.trim(); - } - if (isRecord(directError)) { - if (typeof directError.message === "string" && directError.message.trim()) { - return directError.message.trim(); - } - if (typeof directError.code === "string" && directError.code.trim()) { - return directError.code.trim(); - } - } - return ""; - } - - async function readLocalImageAsDataUrl(filePath: string): Promise { - const trimmedPath = String(filePath || "").trim(); - if (!trimmedPath) { - throw new Error("image_path 不能为空。"); - } - - const existsResult = await Tools.Files.exists(trimmedPath); - if (!existsResult.exists) { - throw new Error(`本地图片不存在: ${trimmedPath}`); - } - - const binaryResult = await Tools.Files.readBinary(trimmedPath); - const base64Content = binaryResult && binaryResult.contentBase64 - ? String(binaryResult.contentBase64).trim() - : ""; - if (!base64Content) { - throw new Error(`读取本地图片失败: ${trimmedPath}`); - } - - return `data:${guessMimeTypeFromPath(trimmedPath)};base64,${base64Content}`; - } - - async function resolveVideoInputs( - imageUrl: string | undefined, - imagePath: string | undefined, - videoUrl: string | undefined - ): Promise<{ image?: string; video_url?: string; input_type: string }> { - const trimmedImageUrl = String(imageUrl || "").trim(); - const trimmedImagePath = String(imagePath || "").trim(); - const trimmedVideoUrl = String(videoUrl || "").trim(); - - const imageInputCount = (trimmedImageUrl ? 1 : 0) + (trimmedImagePath ? 1 : 0); - if (imageInputCount > 1) { - throw new Error("image_url 和 image_path 只能二选一,请不要同时传。"); - } - if (trimmedVideoUrl && imageInputCount > 0) { - throw new Error("视频生成一次只能使用一种输入源:纯文本、图片或视频。请不要同时传 image_* 和 video_url。"); - } - - if (trimmedImageUrl) { - if (!isProbablyUrl(trimmedImageUrl)) { - throw new Error("image_url 必须是 http 或 https 链接。"); - } - return { - image: trimmedImageUrl, - input_type: "image_url" - }; - } - - if (trimmedImagePath) { - return { - image: await readLocalImageAsDataUrl(trimmedImagePath), - input_type: "image_path" - }; - } - - if (trimmedVideoUrl) { - if (!isProbablyUrl(trimmedVideoUrl)) { - throw new Error("video_url 必须是 http 或 https 链接。"); - } - return { - video_url: trimmedVideoUrl, - input_type: "video_url" - }; - } - - return { - input_type: "text" - }; - } - - async function callXaiImageApi(params: { - prompt: string; - model?: string; - size?: string; - }): Promise<{ image_url: string; effective_model: string }> { - const apiKey = getApiKey(); - const modelFromParam = String(params.model || "").trim(); - const effectiveModel = modelFromParam || getDefaultImageModel(); - - const body: JsonMap = { - model: effectiveModel, - prompt: params.prompt - }; - - const size = String(params.size || "").trim(); - if (size) { - body.size = size; - } - - const request = client - .newRequest() - .url(IMAGE_API_ENDPOINT) - .method("POST") - .headers({ - "accept": "application/json", - "content-type": "application/json", - "Authorization": `Bearer ${apiKey}` - }) - .body(JSON.stringify(body), "json"); - - const response = await request.build().execute(); - if (!response.isSuccessful()) { - throw new Error(`xAI 图片 API 调用失败: ${response.statusCode} - ${response.content}`); - } - - const parsed = await parseJsonResponse(response, "xAI 图片生成"); - const data = Array.isArray(parsed.data) ? parsed.data : []; - const first = data.length > 0 && isRecord(data[0]) ? data[0] as JsonMap : null; - const imageUrl = first && typeof first.url === "string" ? String(first.url).trim() : ""; - - if (!imageUrl) { - throw new Error("xAI 响应中未找到图片 URL,请检查模型和参数是否正确。"); - } - - return { - image_url: imageUrl, - effective_model: effectiveModel - }; - } - - async function createVideoTask(params: VideoParams & { image?: string; video_url?: string }) { - const apiKey = getApiKey(); - const modelFromParam = String(params.model || "").trim(); - const effectiveModel = modelFromParam || getDefaultVideoModel(); - - const body: JsonMap = { - model: effectiveModel, - prompt: String(params.prompt || "").trim() - }; - - const isVideoEdit = Boolean(params.video_url); - if (!isVideoEdit) { - body.aspect_ratio = normalizeVideoAspectRatio(params.aspect_ratio); - body.resolution = normalizeVideoResolution(params.resolution); - body.duration = normalizeVideoDuration(params.duration); - } - - if (params.image) { - body.image = { - url: params.image - }; - } - if (params.video_url) { - body.video_url = params.video_url; - } - - const request = client - .newRequest() - .url(VIDEO_GENERATION_ENDPOINT) - .method("POST") - .headers({ - "accept": "application/json", - "content-type": "application/json", - "Authorization": `Bearer ${apiKey}` - }) - .body(JSON.stringify(body), "json"); - - const response = await request.build().execute(); - if (!response.isSuccessful()) { - throw new Error(`xAI 视频创建任务失败: ${response.statusCode} - ${response.content}`); - } - - const parsed = await parseJsonResponse(response, "xAI 视频创建"); - const requestId = typeof parsed.request_id === "string" ? parsed.request_id.trim() : ""; - const status = typeof parsed.status === "string" ? parsed.status.trim() : ""; - - if (!requestId) { - throw new Error(`xAI 视频创建响应中未找到 request_id: ${response.content}`); - } - - return { - request_id: requestId, - status, - effective_model: effectiveModel, - aspect_ratio: typeof body.aspect_ratio === "string" ? body.aspect_ratio : null, - resolution: typeof body.resolution === "string" ? body.resolution : null, - duration: typeof body.duration === "number" ? body.duration : null - }; - } - - async function queryVideoTaskStatus(requestId: string): Promise<{ - status: string; - video_url: string; - content_type: string; - expires_at: string; - duration: number | null; - respect_moderation: string; - error_message: string; - }> { - const apiKey = getApiKey(); - const endpoint = `${API_BASE_URL}/videos/${encodeURIComponent(requestId)}`; - const request = client - .newRequest() - .url(endpoint) - .method("GET") - .headers({ - "accept": "application/json", - "Authorization": `Bearer ${apiKey}` - }); - - const response = await request.build().execute(); - if (!response.isSuccessful()) { - throw new Error(`xAI 视频查询失败: ${response.statusCode} - ${response.content}`); - } - - const parsed = await parseJsonResponse(response, "xAI 视频查询"); - const status = typeof parsed.status === "string" ? parsed.status.trim() : ""; - const video = isRecord(parsed.video) ? parsed.video as JsonMap : null; - const videoUrl = video && typeof video.url === "string" ? video.url.trim() : ""; - const contentType = video && typeof video.content_type === "string" ? video.content_type.trim() : ""; - const expiresAt = video && typeof video.expires_at === "string" ? video.expires_at.trim() : ""; - const duration = video && typeof video.duration === "number" ? video.duration : null; - const respectModeration = video && typeof video.respect_moderation === "string" - ? video.respect_moderation.trim() - : ""; - const errorMessage = extractApiErrorMessage(parsed); - - return { - status, - video_url: videoUrl, - content_type: contentType, - expires_at: expiresAt, - duration, - respect_moderation: respectModeration, - error_message: errorMessage - }; - } - - function normalizeVideoTaskStatus(status: string): "processing" | "completed" | "failed" { - const normalized = String(status || "").trim().toLowerCase(); - if (normalized === "done") return "completed"; - if (normalized === "expired") return "failed"; - return "processing"; - } - - async function draw_image(params: { - prompt: string; - model?: string; - size?: string; - file_name?: string; - }) { - if (!params || !params.prompt || params.prompt.trim().length === 0) { - throw new Error("参数 prompt 不能为空。"); - } - - const prompt = params.prompt.trim(); - await ensureDirectories(); - - const apiResult = await callXaiImageApi({ - prompt, - model: params.model, - size: params.size - }); - - const ext = guessExtensionFromUrl(apiResult.image_url, "png"); - const baseName = buildFileName(prompt, params.file_name, "xai_draw"); - const filePath = `${DRAWS_DIR}/${baseName}.${ext}`; - const downloadResult = await Tools.Files.download(apiResult.image_url, filePath); - if (!downloadResult.successful) { - throw new Error(`下载图片失败: ${downloadResult.details}`); - } - - const fileUri = `file://${filePath}`; - const markdown = `![AI生成的图片](${fileUri})`; - - const hintLines: string[] = []; - hintLines.push(`图片已生成并保存在本地 ${DRAWS_DIR}。`); - hintLines.push(`本地路径: ${filePath}`); - hintLines.push(""); - hintLines.push("在后续回答中,请直接输出下面这一行 Markdown 来展示这张图片:"); - hintLines.push(""); - hintLines.push(markdown); - - return { - prompt, - model: apiResult.effective_model, - remote_image_url: apiResult.image_url, - file_path: filePath, - file_uri: fileUri, - markdown, - hint: hintLines.join("\n") - }; - } - - async function draw_video(params: VideoParams) { - const prompt = String(params && params.prompt ? params.prompt : "").trim(); - if (!prompt) { - throw new Error("prompt 不能为空。"); - } - - await ensureDirectories(); - - const resolvedInputs = await resolveVideoInputs(params.image_url, params.image_path, params.video_url); - const createdTask = await createVideoTask({ - ...params, - prompt, - image: resolvedInputs.image, - video_url: resolvedInputs.video_url - }); - - const pollIntervalMs = normalizePositiveInteger(params.poll_interval_ms, DEFAULT_POLL_INTERVAL_MS); - const maxWaitTimeMs = normalizePositiveInteger(params.max_wait_time_ms, DEFAULT_MAX_WAIT_TIME_MS); - const deadline = Date.now() + maxWaitTimeMs; - - let latestStatus = createdTask.status; - let latestErrorMessage = ""; - let remoteVideoUrl = ""; - let remoteContentType = ""; - let expiresAt = ""; - let remoteDuration = createdTask.duration; - let remoteRespectModeration = ""; - - while (Date.now() <= deadline) { - const statusResult = await queryVideoTaskStatus(createdTask.request_id); - latestStatus = statusResult.status; - latestErrorMessage = statusResult.error_message; - remoteContentType = statusResult.content_type; - expiresAt = statusResult.expires_at; - remoteDuration = statusResult.duration; - remoteRespectModeration = statusResult.respect_moderation; - - const normalizedStatus = normalizeVideoTaskStatus(statusResult.status); - if (normalizedStatus === "completed") { - remoteVideoUrl = statusResult.video_url; - if (!remoteVideoUrl) { - throw new Error("视频任务已完成,但响应中未找到 video.url。"); - } - break; - } - - if (normalizedStatus === "failed") { - throw new Error(`视频生成失败或过期: ${latestErrorMessage || latestStatus || "接口未返回失败原因"}`); - } - - await Tools.System.sleep(pollIntervalMs); - } - - if (!remoteVideoUrl) { - throw new Error( - `视频生成超时,最后状态为 ${latestStatus || "未知"}${latestErrorMessage ? `,原因:${latestErrorMessage}` : ""}` - ); - } - - const extension = guessExtensionFromUrl(remoteVideoUrl, "mp4"); - const baseName = buildFileName(prompt, params.file_name, "xai_video"); - const filePath = `${VIDEOS_DIR}/${baseName}.${extension}`; - const downloadResult = await Tools.Files.download(remoteVideoUrl, filePath); - if (!downloadResult.successful) { - throw new Error(`下载视频失败: ${downloadResult.details}`); - } - - const fileUri = `file://${filePath}`; - const markdownLink = `[点击查看生成的视频](${fileUri})`; - const htmlVideo = ``; - - const hintLines: string[] = []; - hintLines.push(`视频已生成并保存在本地 ${VIDEOS_DIR}。`); - hintLines.push(`本地路径: ${filePath}`); - hintLines.push(""); - hintLines.push("后续回答如果要给出视频,请优先返回这个本地链接:"); - hintLines.push(markdownLink); - hintLines.push(""); - hintLines.push("如果当前渲染环境支持 HTML 视频标签,也可以使用:"); - hintLines.push(htmlVideo); - - return { - prompt, - model: createdTask.effective_model, - request_id: createdTask.request_id, - status: latestStatus, - input_type: resolvedInputs.input_type, - aspect_ratio: createdTask.aspect_ratio, - resolution: createdTask.resolution, - duration: remoteDuration, - remote_video_url: remoteVideoUrl, - remote_content_type: remoteContentType, - remote_expires_at: expiresAt, - remote_respect_moderation: remoteRespectModeration, - file_path: filePath, - file_uri: fileUri, - markdown_link: markdownLink, - html_video: htmlVideo, - hint: hintLines.join("\n") - }; - } - - async function draw_image_wrapper(params: { - prompt: string; - model?: string; - size?: string; - file_name?: string; - }) { - try { - const result = await draw_image(params); - complete({ - success: true, - message: "xAI 图片生成成功,已下载到本地。", - data: result - }); - } catch (error) { - console.error("draw_image 执行失败:", error); - complete({ - success: false, - message: `xAI 图片生成失败: ${getErrorMessage(error)}`, - error_stack: getErrorStack(error) - }); - } - } - - async function draw_video_wrapper(params: VideoParams) { - try { - const result = await draw_video((params || {}) as VideoParams); - complete({ - success: true, - message: "xAI 视频生成成功,已下载到本地。", - data: result - }); - } catch (error) { - console.error("draw_video 执行失败:", error); - complete({ - success: false, - message: `xAI 视频生成失败: ${getErrorMessage(error)}`, - error_stack: getErrorStack(error) - }); - } - } - - return { - draw_image: draw_image_wrapper, - draw_video: draw_video_wrapper - }; -})(); - -exports.draw_image = xaiDraw.draw_image; -exports.draw_video = xaiDraw.draw_video; diff --git a/scripts/zhipu_draw.ts b/scripts/zhipu_draw.ts deleted file mode 100644 index 7630033..0000000 --- a/scripts/zhipu_draw.ts +++ /dev/null @@ -1,262 +0,0 @@ -/* METADATA -{ - "name": "zhipu_draw", - "display_name": { - "zh": "智谱生图", - "en": "Zhipu Draw" - }, - "description": { - "zh": "使用智谱AI图像生成API根据提示词画图,将图片保存到本地 /sdcard/Download/Operit/plugins/draw/zhipu_draw/draws/ 目录,并返回 Markdown 图片提示。", - "en": "Generate images via Zhipu AI image generation API from a prompt, save to /sdcard/Download/Operit/plugins/draw/zhipu_draw/draws/, and return a Markdown image reference." - }, - "env": [ - { - "name": "ZHIPU_API_KEY", - "description": { - "zh": "智谱API Key(必填)", - "en": "Zhipu API key (required)" - }, - "required": true - }, - { - "name": "ZHIPU_IMAGE_MODEL", - "description": { - "zh": "默认绘图模型(可选;当 draw_image 未传 model 时使用)", - "en": "Default image model (optional; used when draw_image doesn't pass model)" - }, - "required": false - } - ], - "category": "Draw", - "tools": [ - { - "name": "draw_image", - "description": { - "zh": "根据提示词调用智谱AI图像生成接口生成图片,保存到本地并返回 Markdown 图片提示。", - "en": "Generate an image via Zhipu AI image generation API using a prompt, save it locally, and return a Markdown image reference." - }, - "parameters": [ - { "name": "prompt", "description": { "zh": "绘图提示词(中文或英文)", "en": "Prompt for image generation (Chinese or English)" }, "type": "string", "required": true }, - { "name": "size", "description": { "zh": "图片尺寸,例如 '1024x1024' 或 '1280x1280'(可选,默认1024x1024)", "en": "Image size, e.g. '1024x1024' or '1280x1280' (optional, default 1024x1024)" }, "type": "string", "required": false }, - { "name": "file_name", "description": { "zh": "自定义保存到本地的文件名(不含路径和扩展名)", "en": "Custom output file name (without path or extension)" }, "type": "string", "required": false }, - { "name": "model", "description": { "zh": "模型名称(可选;不传则使用环境变量 ZHIPU_IMAGE_MODEL,否则默认glm-image)", "en": "Model name (optional; falls back to env ZHIPU_IMAGE_MODEL)" }, "type": "string", "required": false } - ] - } - ] -}*/ - -/// - -const zhipuDraw = (function () { - const HTTP_TIMEOUT_MS = 600000; - const client = OkHttp.newBuilder() - .connectTimeout(HTTP_TIMEOUT_MS) - .readTimeout(HTTP_TIMEOUT_MS) - .writeTimeout(HTTP_TIMEOUT_MS) - .build(); - - const API_BASE_URL = "https://open.bigmodel.cn/api/paas/v4/images/generations"; - const DRAW_ROOT_DIR = getPluginConfigDir("draw"); - const STORAGE_DIR = `${DRAW_ROOT_DIR}/zhipu_draw`; - const DRAWS_DIR = `${STORAGE_DIR}/draws`; - - interface DrawImageParams { - prompt: string; - size?: string; - file_name?: string; - model?: string; - } - - interface ZhipuImageApiResult { - url: string; - model?: string; - } - - interface DrawImageResult { - file_path: string; - file_uri: string; - markdown: string; - prompt: string; - hint: string; - } - - function getErrorMessage(error: unknown): string { - if (error instanceof Error) return error.message; - return String(error); - } - - function getErrorStack(error: unknown): string | undefined { - if (error instanceof Error) return error.stack; - return undefined; - } - - function getApiKey(): string { - const apiKey = getEnv("ZHIPU_API_KEY"); - if (!apiKey) { - throw new Error("ZHIPU_API_KEY 未配置,请在环境变量中设置智谱的 API Key。"); - } - return apiKey; - } - - function sanitizeFileName(name: string): string { - const safe = name.replace(/[\\/:*?"<>|]/g, "_").trim(); - if (!safe) { - return `zhipu_draw_${Date.now()}`; - } - return safe.substring(0, 80); - } - - function buildFileName(prompt: string, customName?: string | null): string { - if (customName && customName.trim().length > 0) { - return sanitizeFileName(customName); - } - const shortPrompt = prompt.length > 40 ? `${prompt.substring(0, 40)}...` : prompt; - const base = sanitizeFileName(shortPrompt || "image"); - const timestamp = Date.now(); - return `${base}_${timestamp}`; - } - - async function ensureDirectories(): Promise { - const dirs = [DRAW_ROOT_DIR, STORAGE_DIR, DRAWS_DIR]; - for (const dir of dirs) { - try { - const result = await Tools.Files.mkdir(dir); - if (!result.successful) { - console.warn(`创建目录失败(可能已存在): ${dir} -> ${result.details}`); - } - } catch (e: unknown) { - console.warn(`创建目录异常: ${dir} -> ${getErrorMessage(e)}`); - } - } - } - - async function callZhipuImageAPI(params: { - prompt: string; - size?: string; - model?: string; - }): Promise { - const apiKey = getApiKey(); - const modelFromParam = (params.model || "").trim(); - const modelFromEnv = (getEnv("ZHIPU_IMAGE_MODEL") || "").trim(); - const effectiveModel = modelFromParam || modelFromEnv || "glm-image"; - - const body = { - model: effectiveModel, - prompt: params.prompt, - size: params.size || "1024x1024" - }; - - const headers = { - "accept": "application/json", - "content-type": "application/json", - "Authorization": `Bearer ${apiKey}` - }; - - const request = client - .newRequest() - .url(API_BASE_URL) - .method("POST") - .headers(headers) - .body(JSON.stringify(body), "json"); - - const response = await request.build().execute(); - - if (!response.isSuccessful()) { - throw new Error(`智谱图片 API 调用失败: ${response.statusCode} - ${response.content}`); - } - - let parsed: unknown; - try { - parsed = JSON.parse(response.content) as unknown; - } catch (e: unknown) { - throw new Error(`解析智谱响应失败: ${getErrorMessage(e)}`); - } - - const first = (parsed as any)?.data?.[0]; - if (!first || !first.url) { - throw new Error("智谱响应中未找到有效的图片数据,请检查API返回格式"); - } - - const result: ZhipuImageApiResult = { - url: String(first.url), - model: first.model ? String(first.model) : undefined - }; - - if (!result.model) { - result.model = effectiveModel; - } - - return result; - } - - async function draw_image(params: DrawImageParams): Promise { - if (!params || !params.prompt || params.prompt.trim().length === 0) { - throw new Error("参数 prompt 不能为空。"); - } - - const prompt = params.prompt.trim(); - const size = params.size || "1024x1024"; - const model = params.model; - - await ensureDirectories(); - - const apiResult = await callZhipuImageAPI({ prompt, size, model }); - - if (!apiResult.url || !apiResult.url.trim()) { - throw new Error("API响应中未包含图片URL"); - } - - const baseName = buildFileName(prompt, params.file_name); - const filePath = `${DRAWS_DIR}/${baseName}.png`; - - const downloadResult = await Tools.Files.download(apiResult.url, filePath); - if (!downloadResult.successful) { - throw new Error(`下载图片失败: ${downloadResult.details}`); - } - - const fileUri = `file://${filePath}`; - const markdown = `![AI生成的图片](${fileUri})`; - - const hintLines: string[] = []; - hintLines.push(`图片已生成并保存在本地 ${DRAWS_DIR}。`); - hintLines.push(`本地路径: ${filePath}`); - hintLines.push(`提示词: ${prompt}`); - hintLines.push(`模型: ${apiResult.model || "glm-image"}`); - hintLines.push(""); - hintLines.push("在后续回答中,请直接输出下面这一行 Markdown 来展示这张图片:"); - hintLines.push(""); - hintLines.push(markdown); - - return { - file_path: filePath, - file_uri: fileUri, - markdown, - prompt, - hint: hintLines.join("\n") - }; - } - - async function draw_image_wrapper(params: DrawImageParams) { - try { - const result = await draw_image(params); - complete({ - success: true, - message: `图片生成成功,已保存到 ${DRAWS_DIR},并返回 Markdown 图片提示。`, - data: result - }); - } catch (error: unknown) { - console.error("draw_image 执行失败:", error); - complete({ - success: false, - message: `图片生成失败: ${getErrorMessage(error)}`, - error_stack: getErrorStack(error) - }); - } - } - - return { - draw_image: draw_image_wrapper - }; -})(); - -exports.draw_image = zhipuDraw.draw_image; diff --git a/scripts/zhipu_search.ts b/scripts/zhipu_search.ts deleted file mode 100644 index 4420c9b..0000000 --- a/scripts/zhipu_search.ts +++ /dev/null @@ -1,237 +0,0 @@ -/* METADATA -{ - "name": "zhipu_search", - "display_name": { - "zh": "智谱搜索", - "en": "Zhipu Search" - }, - "description": { - "zh": "智谱 AI 独立网络搜索 API,返回结构化搜索结果。", - "en": "Zhipu AI standalone web search API with structured results." - }, - "author": "浮生一梦", - "env": [ - { - "name": "ZHIPU_SEARCH_API_KEY", - "description": { - "zh": "智谱搜索专用 API Key(与生图 Key 独立)", - "en": "Zhipu Search API Key (independent from draw Key)" - }, - "required": false - } - ], - "category": "Search", - "tools": [ - { - "name": "search", - "description": { - "zh": "使用智谱 Web Search API 进行搜索", - "en": "Search using Zhipu Web Search API" - }, - "parameters": [ - { "name": "query", "description": { "zh": "搜索关键词", "en": "Search query" }, "type": "string", "required": true }, - { "name": "api_key", "description": { "zh": "智谱 API Key(可选,不传则读取环境变量)", "en": "Zhipu API Key" }, "type": "string", "required": false }, - { "name": "engine", "description": { "zh": "搜索引擎:search_std/search_pro/search_pro_sogou/search_pro_quark", "en": "Search engine" }, "type": "string", "required": false }, - { "name": "count", "description": { "zh": "返回结果数 (1-50),默认 10", "en": "Result count (1-50)" }, "type": "number", "required": false }, - { "name": "recency", "description": { "zh": "时间范围:oneDay/oneWeek/oneMonth/oneYear/noLimit", "en": "Time range" }, "type": "string", "required": false }, - { "name": "content_size", "description": { "zh": "内容长度:medium/high", "en": "Content size" }, "type": "string", "required": false } - ] - }, - { - "name": "test", - "description": { - "zh": "测试 API 连接", - "en": "Test API connection" - }, - "parameters": [] - } - ] -}*/ - -/// - -const zhipuSearch = (function () { - const API_URL = "https://open.bigmodel.cn/api/paas/v4/web_search"; - const TIMEOUT = 60000; - const client = OkHttp.newBuilder() - .connectTimeout(TIMEOUT) - .readTimeout(TIMEOUT) - .writeTimeout(TIMEOUT) - .build(); - - type SearchParams = { - query: string; - api_key?: string; - engine?: string; - count?: number; - recency?: string; - content_size?: string; - }; - - type ZhipuSearchItem = { - title?: string; - content?: string; - link?: string; - media?: string; - icon?: string; - publish_date?: string; - refer?: string; - }; - - type ZhipuSearchResponse = { - id?: string; - search_result?: ZhipuSearchItem[]; - search_intent?: string[]; - }; - - type SearchResult = { - success: true; - query: string; - id?: string; - intent: string | null; - results: ZhipuSearchItem[]; - count: number; - }; - - type TestResult = { - success: true; - latency: number; - id?: string; - }; - - function getApiKey(providedKey?: string): string { - if (providedKey) return providedKey; - let key = getEnv("ZHIPU_SEARCH_API_KEY"); - if (key) return key; - key = getEnv("DEFAULT_API_KEY"); - return key || ""; - } - - async function httpPost(body: Record, apiKey?: string | null): Promise { - const key = getApiKey(apiKey || undefined); - if (!key) { - throw new Error("未设置 API Key,请配置 ZHIPU_SEARCH_API_KEY 环境变量或在调用时传入 api_key 参数"); - } - - const request = client - .newRequest() - .url(API_URL) - .method("POST") - .header("Authorization", "Bearer " + key) - .header("Content-Type", "application/json") - .body(JSON.stringify(body), "json"); - - const response = await request.build().execute(); - const content = response.content; - - if (!response.isSuccessful()) { - throw new Error("HTTP " + response.statusCode + ": " + content); - } - - return JSON.parse(content) as ZhipuSearchResponse; - } - - async function search(params: SearchParams): Promise { - const body: Record = { - search_query: params.query, - search_engine: params.engine || "search_std", - search_intent: true, - count: params.count || 10, - content_size: params.content_size || "medium" - }; - - if (params.recency) { - body.search_recency_filter = params.recency; - } - - const result = await httpPost(body, params.api_key); - - let results: ZhipuSearchItem[] = []; - if (result.search_result && Array.isArray(result.search_result)) { - results = result.search_result.map((item) => ({ - title: item.title, - content: item.content, - link: item.link, - media: item.media, - icon: item.icon, - publish_date: item.publish_date, - refer: item.refer - })); - } - - let intent: string | null = null; - if (result.search_intent && result.search_intent.length > 0) { - intent = result.search_intent[0] || null; - } - - return { - success: true, - query: params.query, - id: result.id, - intent, - results, - count: results.length - }; - } - - async function test(): Promise { - const body = { - search_query: "hi", - search_engine: "search_std", - search_intent: false, - count: 1 - }; - - const start = Date.now(); - const result = await httpPost(body, null); - const latency = Date.now() - start; - - return { - success: true, - latency, - id: result.id - }; - } - - async function searchWrapper(params: SearchParams) { - try { - const result = await search(params); - complete({ - success: true, - message: "搜索完成,找到 " + result.count + " 条结果", - data: result - }); - } catch (error: any) { - complete({ - success: false, - message: "搜索失败:" + error.message, - error_stack: error.stack - }); - } - } - - async function testWrapper() { - try { - const result = await test(); - complete({ - success: true, - message: "连接成功,延迟 " + result.latency + "ms", - data: result - }); - } catch (error: any) { - complete({ - success: false, - message: "测试失败:" + error.message, - error_stack: error.stack - }); - } - } - - return { - search: searchWrapper, - test: testWrapper - }; -})(); - -exports.search = zhipuSearch.search; -exports.test = zhipuSearch.test;