diff --git a/plugins/Fectivnfy112357/crossplug/LICENSE b/plugins/Fectivnfy112357/crossplug/LICENSE new file mode 100644 index 0000000..125be1b --- /dev/null +++ b/plugins/Fectivnfy112357/crossplug/LICENSE @@ -0,0 +1,192 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + Copyright 2026 MCode Plugins contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/plugins/Fectivnfy112357/crossplug/README.md b/plugins/Fectivnfy112357/crossplug/README.md new file mode 100644 index 0000000..74825f3 --- /dev/null +++ b/plugins/Fectivnfy112357/crossplug/README.md @@ -0,0 +1,47 @@ +# crossplug + +Bidirectional plugin converter between **DSH** (DeepSeek Harness) and **mcode** (MiniMax Code / pi). + +## The problem + +Migrating a plugin between AI coding tools usually means rewriting it for the +target tool's plugin system. crossplug converts plugins in **both directions** +while keeping the original logic intact (runtime bridging): the source plugin +code runs unmodified inside the converted package, and every mapping decision +is recorded in `CONVERSION-REPORT.md`. + +## Try it + +Install the plugin from `/plugins` → **Local**, then use the slash command: + +```text +/convert-plugin dsh2mcode ~/.dsh/.agent-presets/my-agent +``` + +**Expected result**: an agent-plugins.org 1.0.0 package (`plugin.json` + +`skills/`) in `./out/`, ready to copy to `~/.minimax/plugins//`, plus a +`CONVERSION-REPORT.md` explaining every mapped row. + +Other directions: + +```text +/convert-plugin mcode2dsh ./some-mcode-plugin # pi/mcode extension → DSH agent preset +/convert-plugin mcode2dsh ./some-mcode-plugin --host # → DSH host plugin (all sessions) +/convert-list # list plugins on both sides +``` + +## Requirements + +- Node.js 18+ (the converter core is zero-dependency CommonJS) +- For mcode output: MiniMax Code with Agent Plugins 1.0 support +- For DSH output: DeepSeek Harness with agent presets or host composition + +## Data and network + +- **No network access.** Conversion is fully local (reads local files only). +- **No credentials required.** +- No telemetry, no external services. + +## License + +Apache-2.0 diff --git a/plugins/Fectivnfy112357/crossplug/core/dsh2mcode.js b/plugins/Fectivnfy112357/crossplug/core/dsh2mcode.js new file mode 100644 index 0000000..f9aed4e --- /dev/null +++ b/plugins/Fectivnfy112357/crossplug/core/dsh2mcode.js @@ -0,0 +1,1233 @@ +// dsh2mcode.js — DSH 插件 → mcode(MiniMax Code / pi)插件包(零依赖) +// +// 两种输入: +// 1. DSH agent preset 目录(preset.yml + agent.cordis.yml [+ skills/]) +// → 生成 mcode 插件包:package.json + extension.js(pi extension)+ 报告 +// 2. DSH 插件源码文件(动态 Cordis 插件 / preset 本地插件 JS) +// → 生成 pi extension:提取 registerTool/defineTool 与 ctx.on 并转写 +// +// 转换是"脚手架 + 映射",不是完全自动等价:无法映射的能力生成带说明的桩。 + +'use strict'; + +const fs = require('node:fs'); +const path = require('node:path'); +const { parsePreset } = require('./yaml.js'); +const { classifyRow } = require('./mapping.js'); +const { jsonSchemaToTypeboxCode, typeboxShimCode } = require('./schema.js'); +const { findCalls, propertyValue, jsLiteralToValue } = require('./extract.js'); + +const TOOL_REGEXES = [ + /harness\.defineTool/g, + /harness\.registerTool/g, + /tools\.register/g, + /defineTool/g, + /registerTool/g, +]; + +// DSH 事件 ↔ pi 事件对照表(2026-08-14 对照 dsh-plugin-spec.md §9 权威会话事件词汇修正) +// +// DSH 侧事件分两类: +// 1. ctx.on 总线事件(插件直接监听,独立于会话日志): +// system-prompt/assemble、session/event、skills/change +// 2. session/event 流的子事件(ctx.on('session/event', (session, event)), +// 按 event.type 过滤,payload 在 event.data —— KNOWN_SESSION_EVENT_TYPES): +// turn/start、turn/end、step/start、step/end、user/message、 +// assistant/message、tool/call、tool/result、todo/write、 +// agent-preset/selected、command/run、command/done、compaction/* … +// +// pi 事件名以 mcode-plugin-spec.md §5.2 为准。 +// bridgeKind:assembly = system-prompt/assemble 形态;turn-end = session/event(turn/end) 形态。 +const EVENT_MAP = { + // ── 1:1 可桥接(生成真正的 pi.on wrapper,调原 DSH handler)── + 'system-prompt/assemble': { + pi: 'before_agent_start', + bridge: true, + bridgeKind: 'assembly', + note: 'DSH host 平面每会话组装系统提示,handler(assembly, context, next) 往 assembly.sections.push({name, text});pi 的 before_agent_start 在每轮 agent 循环前触发,返回 { systemPrompt } 即注入增补(mcode 文档 §5.2 1:1)', + }, + 'session/event': { + pi: 'turn_end', + bridge: true, + bridgeKind: 'turn-end', + note: 'DSH handler(session, event) 按 event.type 过滤子事件(turn/end 驱动 auto-retain 等);pi 的 turn_end 在每轮 LLM 响应 + 工具调用完成后触发,桥接为 turn/end 子事件(mcode 文档 §5.2 1:1)', + }, + // ── 弱对应(生成 stub;语义不直通,需人工移植)── + 'tool/call': { pi: 'tool_call', note: 'DSH 是会话日志子事件(event.type === "tool/call",append-only 不可拦截);pi 的 tool_call 可 block。语义不一致,生成 stub' }, + 'tool/result': { pi: 'tool_result', note: 'DSH 是会话日志子事件(event.type === "tool/result");pi 的 tool_result 是 middleware 可改结果。语义不一致,生成 stub' }, + 'user/message': { pi: 'message_start', note: 'DSH user/message 是用户消息落库子事件;pi message_start 是消息生命周期,弱对应' }, + 'assistant/message': { pi: 'message_end', note: 'DSH assistant/message 是助手消息落库子事件;pi message_end 可替换消息,弱对应' }, + 'turn/start': { pi: 'turn_start', note: 'DSH turn/start 开轮子事件;pi turn_start 每轮开始,弱对应' }, + 'agent-preset/selected': { pi: 'model_select', note: 'DSH 会话切换 preset(仅日志);pi model_select 模型切换,弱对应' }, + 'compaction/start': { pi: 'session_before_compact', note: 'DSH 压缩开始子事件;pi 压缩前可取消/自定义 summary,概念对应' }, + 'compaction/end': { pi: 'session_compact', note: 'DSH 压缩完成子事件;pi 压缩完成,概念对应' }, + 'skills/change': { pi: 'resources_discover', note: 'DSH 技能目录变更失效通知(ctx.on 总线,emit 模式);pi resources_discover 启动/重载时贡献 skillPaths(reason: startup|reload),目录级而非变更级,弱对应' }, + // ── 旧名兼容(deprecated:DSH 侧不存在这些事件,仅避免旧插件转换产物误报)── + 'tools/pre-execute': { pi: 'tool_call', note: '[deprecated 事件名] DSH 真实事件是 tool/call(session/event 子事件,append-only);请源插件改用真实事件名' }, + 'tools/result': { pi: 'tool_result', note: '[deprecated 事件名] DSH 真实事件是 tool/result;请源插件改用真实事件名' }, + 'message/send': { pi: 'message_start', note: '[deprecated 事件名] DSH 真实事件是 user/message / assistant/message;请源插件改用真实事件名' }, +}; + + +// 从源文件提取插件 name 字段(兼容 export default { name: 'x' } 和 const name = 'x') +function extractPluginName(source) { + const re1 = /export\s+default\s*\{[\s\S]*?name\s*:\s*['"]([^'"]+)['"]/; + const re2 = /(?:^|\n)\s*(?:const|let|var)\s+name\s*=\s*['"]([^'"]+)['"]/; + const m1 = re1.exec(source); + if (m1) return m1[1]; + const m2 = re2.exec(source); + if (m2) return m2[1]; + return null; +} + +// 从源文件提取 inject 字段 +function extractInject(source) { + const m = /inject\s*:\s*\[([^\]]*)\]/.exec(source); + if (!m) return null; + return m[1].split(',').map((s) => s.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean); +} + +// 把 DSH 非标准 schema 规范化成标准 JSON Schema +// DSH: { query: { type: 'string', required: true } } +// JSON Schema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] } +function normalizeDshSchema(text) { + if (!text || typeof text !== 'string') return null; + let obj; + try { obj = jsLiteralToValue(text); } + catch { try { obj = JSON.parse(text); } catch { return null; } } + if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return obj; + // 已经是标准 JSON Schema(有 type 字段) + if (obj.type) return obj; + // DSH 形态:每个属性自己带 type + const props = {}; + const required = []; + for (const [k, v] of Object.entries(obj)) { + if (v && typeof v === 'object' && 'type' in v) { + const { required: isReq, ...rest } = v; + props[k] = rest; + if (isReq === true) required.push(k); + } else { + props[k] = v; + } + } + return { type: 'object', properties: props, ...(required.length ? { required } : {}) }; +} + +function slugify(id) { + return String(id || 'plugin') + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + .slice(0, 48) || 'plugin'; +} + +// agent-plugins.org 插件名规则:/^(?!.*(?:--|\.\.))[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/ +function makePluginName(humanName, fallbackId) { + const ascii = String(humanName || '') + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/--+/g, '-') + .replace(/^-+|-+$/g, '') + .slice(0, 48); + let name = ascii || slugify(fallbackId); + if (!name) name = 'dsh-plugin'; + name = name.replace(/--+/g, '-').replace(/\.\.+/g, '.').replace(/[.-]$/g, ''); + if (!/^[a-z0-9]/.test(name)) name = 'd-' + name; + return name; +} + +// skill 目录名规则:/^(?!.*--)[a-z0-9]+(?:-[a-z0-9]+)*$/ +function safeSkillName(name) { + let n = String(name || '') + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/--+/g, '-') + .replace(/^-+|-+$/g, ''); + if (!n) n = 'skill'; + return n; +} + +function writeFileSafe(file, content) { + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, content, 'utf8'); +} + +function copyDir(src, dst) { + if (!fs.existsSync(src)) return; + fs.mkdirSync(dst, { recursive: true }); + for (const entry of fs.readdirSync(src, { withFileTypes: true })) { + const s = path.join(src, entry.name); + const d = path.join(dst, entry.name); + if (entry.isDirectory()) copyDir(s, d); + else if (entry.isFile()) fs.copyFileSync(s, d); + } +} + +// ═══ 模式一:DSH agent preset → mcode 插件包 ═══ + +function convertPreset(srcDir, outDir) { + const presetPath = path.join(srcDir, 'preset.yml'); + const compositionPath = path.join(srcDir, 'agent.cordis.yml'); + if (!fs.existsSync(presetPath)) throw new Error(`preset.yml 不存在: ${presetPath}`); + if (!fs.existsSync(compositionPath)) throw new Error(`agent.cordis.yml 不存在: ${compositionPath}`); + + const preset = parsePreset( + fs.readFileSync(presetPath, 'utf8'), + fs.readFileSync(compositionPath, 'utf8'), + { presetPath, compositionPath }, + ); + + const id = slugify(preset.name || path.basename(srcDir)); + const pkgDir = outDir; + const rows = preset.rows || []; + const report = []; + const srcSkills = path.join(srcDir, 'skills'); + + // ── 分析行 → 能力 ── + let personaText = ''; + const stubs = []; // { kind: 'tool'|'command', id, name, note, pi } + const notes = []; + + for (const row of rows) { + const mapping = classifyRow(row); + const display = `${row.id} (${row.name})`; + if (mapping.kind === 'persona') { + // 从 config.text 取 persona(支持嵌套 configRows 里找 text) + let text = ''; + if (typeof row.config === 'string') text = row.config; + else if (row.config && typeof row.config === 'object') { + if (row.config.text) text = row.config.text; + if (row.config.__rows) { + for (const sub of row.config.__rows) { + if (sub.text && typeof sub.text === 'string') text += sub.text + '\n'; + } + } + } + personaText = text || ''; + notes.push(`persona(${display}):文本已写入 /persona 命令,${mapping.note}`); + } else if (mapping.kind === 'tool') { + stubs.push({ kind: 'tool', id: slugify(row.id), label: row.id, name: row.name, pi: mapping.pi, note: mapping.note }); + report.push(`[tool] ${display} → ${mapping.pi}`); + } else if (mapping.kind === 'command') { + stubs.push({ kind: 'command', id: slugify(row.id), label: row.id, name: row.name, pi: mapping.pi, note: mapping.note }); + report.push(`[command] ${display} → ${mapping.pi}`); + } else if (mapping.kind === 'warn') { + notes.push(`⚠ ${display}:${mapping.pi}`); + report.push(`[warn] ${display} → ${mapping.pi}`); + } else { + notes.push(`· ${display}:${mapping.pi}`); + report.push(`[note] ${display} → ${mapping.pi}`); + } + } + + // ═══ 生成 AGENT_PLUGINS_V1 插件包(agent-plugins.org 1.0.0)═══ + // MCode(MiniMax Code)本地插件格式:根 plugin.json + skills/ + 可选 mcp.json, + // 安装到 ~/.minimax/plugins// 后出现在 /plugins 的 Local 标签页。 + const pluginName = makePluginName(preset.name, path.basename(srcDir)); + const manifest = { + $schema: 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json', + name: pluginName, + version: '0.1.0', + description: (preset.description || `由 DSH agent preset「${preset.name || path.basename(srcDir)}」转换生成`).slice(0, 500), + author: { name: 'crossplug' }, + keywords: ['dsh', 'converted', 'bridge'], + extensions: { + dshToMcode: { + source: preset.meta.presetPath || srcDir, + sourceKind: 'dsh-agent-preset', + generatedAt: new Date().toISOString(), + }, + }, + }; + writeFileSafe(path.join(pkgDir, 'plugin.json'), JSON.stringify(manifest, null, 2) + '\n'); + + // ── skills/ ── + // 1) persona → skills/persona/SKILL.md(MCode 插件通过 skill 向 agent 提供知识) + if (personaText) { + writeFileSafe(path.join(pkgDir, 'skills', 'persona', 'SKILL.md'), [ + '---', + 'name: persona', + 'description: 来源 DSH agent preset「' + (preset.name || id) + '」的 persona 文本(转换参考)。', + '---', + '', + '这是由 DSH agent preset 转换的插件。以下为来源 preset 的 persona(系统提示)原文:', + '', + '```text', + personaText, + '```', + '', + ].join('\n') + '\n'); + } + // 2) preset 自带 skills/ 复制(目录名按 agent-plugins 规则规范化) + if (fs.existsSync(srcSkills)) { + for (const entry of fs.readdirSync(srcSkills, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + const safe = safeSkillName(entry.name); + copyDir(path.join(srcSkills, entry.name), path.join(pkgDir, 'skills', safe)); + if (safe !== entry.name) { + notes.push(`skill 目录「${entry.name}」重命名为 ${safe}(agent-plugins 名字规则)`); + } + } + } + // 3) 转换说明 skill + writeFileSafe(path.join(pkgDir, 'skills', 'conversion-report', 'SKILL.md'), [ + '---', + 'name: conversion-report', + 'description: 本插件由 DSH agent preset 转换而来(脚手架+映射)。说明来源行与映射结论。', + '---', + '', + '本插件由 crossplug 从 DSH agent preset 转换生成。', + '插件能力承载方式为 skills(+可选的 mcp.json)。DSH preset 中的工具行无法', + '直接映射为 MCode 插件工具(MCode 插件体系为 skills/MCP/apps),逐行映射见', + '包内 CONVERSION-REPORT.md。来源 preset 的 persona 在 skills/persona/。', + '', + ].join('\n') + '\n'); + + // ── 报告 / README ── + writeFileSafe(path.join(pkgDir, 'CONVERSION-REPORT.md'), renderReport(preset, srcDir, report, personaText, stubs, notes)); + writeFileSafe(path.join(pkgDir, 'README.md'), [ + `# ${pluginName}`, + '', + `由 DSH agent preset「${preset.name || id}」转换生成(crossplug),agent-plugins.org 1.0.0 格式。`, + '', + '## 安装到 MiniMax Code(mcode)', + '', + '1. 把本目录复制/安装到 `~/.minimax/plugins//`(即本目录名)。', + '2. 在 MiniMax Code 中打开 `/plugins`,切到 Local 标签页,找到本插件,按 Enter 安装。', + '3. 安装后对话中可调用其 skills(persona / conversion-report / 来源 preset 的技能)。', + '', + '## 转换说明', + '', + '这是"脚手架 + 映射"结果:MCode 插件承载 skills/MCP/apps;DSH 的工具行无法', + '自动移植为插件工具。逐行映射结论见 `CONVERSION-REPORT.md`。', + '', + ].join('\n') + '\n'); + + return { + kind: 'preset', + source: srcDir, + presetName: preset.name || id, + out: pkgDir, + packageName: pluginName, + rows: rows.length, + tools: stubs.filter((s) => s.kind === 'tool').length, + commands: stubs.filter((s) => s.kind === 'command').length, + notes: notes.length, + reportFile: path.join(pkgDir, 'CONVERSION-REPORT.md'), + }; +} + +function renderReport(preset, srcDir, report, personaText, stubs, notes) { + const lines = []; + lines.push(`# 转换报告:DSH preset → mcode 插件包`); + lines.push(''); + lines.push(`- 来源: ${preset.meta.presetPath || srcDir}`); + lines.push(`- 名称: ${preset.name || '-'}`); + lines.push(`- 描述: ${preset.description || '-'}`); + lines.push(`- 生成时间: ${new Date().toISOString()}`); + lines.push(''); + lines.push('## 行映射'); + lines.push(''); + lines.push('| DSH 行 | 转换结果 |'); + lines.push('| --- | --- |'); + for (const r of report) lines.push(`| ${r.split(' → ')[0].replace(/\|/g, '\\|')} | ${(r.split(' → ').slice(1).join(' → ') || '').replace(/\|/g, '\\|')} |`); + lines.push(''); + lines.push('## 桩工具 / 命令'); + lines.push(''); + for (const s of stubs) lines.push(`- \`${s.kind}\` \`${s.id}\`(来源 ${s.name}):${s.pi}${s.note ? ' —— ' + s.note : ''}`); + if (!stubs.length) lines.push('(无)'); + lines.push(''); + lines.push('## 说明项'); + lines.push(''); + for (const n of notes) lines.push(`- ${n}`); + if (!notes.length) lines.push('(无)'); + lines.push(''); + if (personaText) { + lines.push('## persona 文本'); + lines.push(''); + lines.push('```text'); + lines.push(personaText); + lines.push('```'); + } + return lines.join('\n') + '\n'; +} + +// ═══ 模式二:DSH 插件源码 → pi extension ═══ + +function convertPluginSource(srcFile, outDir, opts = {}) { + const source = fs.readFileSync(srcFile, 'utf8'); + const base = path.basename(srcFile, path.extname(srcFile)); + const tools = []; + const events = []; + const lostCapabilities = []; + const warnings = []; + + // ── 提取源插件的 name / inject(用于命名 plugin + 报告)── + const sourcePluginName = extractPluginName(source); + const sourceInject = extractInject(source); + if (sourceInject && sourceInject.length) { + warnings.push(`源插件声明 inject: [${sourceInject.join(', ')}];本次只模拟 ${JSON.stringify(sourceInject.includes('tools') ? ['tools'] : sourceInject)},其他服务(agents / effect / on)调用将不工作`); + } + + // ── 提取工具注册 ── + // harness.defineTool / harness.registerTool / tools.register / defineTool / registerTool + const callRe = /(?:harness\.defineTool|harness\.registerTool|tools\.register|defineTool|registerTool)/g; + const calls = findCalls(source, callRe).filter((c, i, arr) => { + // 去重:跳过被更长模式(如 harness.defineTool 内的 defineTool)覆盖的匹配 + return i === 0 || c.start >= arr[i - 1].end; + }); + for (const call of calls) { + let args = call.argsText; + // harness.registerTool(ctx, {...}) 之类的多参数调用:取第一个顶层 '{' 之后的部分 + if (!args.trim().startsWith('{')) { + const braceIdx = firstTopLevelBrace(args); + if (braceIdx >= 0) args = args.slice(braceIdx); + } + if (!args.trim().startsWith('{')) { + warnings.push(`跳过非对象字面量的注册调用: ${args.slice(0, 60)}`); + continue; + } + const name = propertyValue(args, 'name'); + const description = propertyValue(args, 'description'); + const parameters = propertyValue(args, 'parameters'); + if (!name || name.kind !== 'string') { + warnings.push(`跳过缺少 name 的注册调用(可能 name 是变量)`); + continue; + } + // 工具元数据保留下来(name/description/parameters); + // execute 不再字符串内联——改为 vendor 加载 + 模拟 ctx(见 mcp-server.js / extension.js 生成处)。 + tools.push({ + name: name.value, + description: description && description.kind === 'string' ? description.value : '', + parametersText: parameters && parameters.kind === 'code' ? parameters.value : '{}', + }); + } + + // 2) ctx.on('event', ...) 事件 — 用独立 regex 抽取事件名(不走 findCalls/matchParen, + // 因为含 lambda body 的 ctx.on 的 args 含 regex literal + '\u200b' 字符串,matchParen 词法分析扛不住) + const EVENT_NAME_RE = /(?:ctx|host|agent|harness|engine)\.on\s*\(\s*(['"])([^'"]+)\1/g; + for (const m of source.matchAll(EVENT_NAME_RE)) { + const eventName = m[2]; + if (!eventName) continue; + if (events.find((e) => e.event === eventName)) continue; // 去重 + const mapping = EVENT_MAP[eventName]; + events.push({ event: eventName }); + if (!mapping) { + lostCapabilities.push(`ctx.on('${eventName}', ...):mcode 无对应事件,自动触发该能力在转换后丢失(请改手动调用工具或保留 DSH 侧挂载)`); + warnings.push(`事件 ${eventName} 无 mcode 对应,转换后该能力丢失`); + } else if (mapping.bridge) { + // 可桥接:会在 extension.js 里生成真正的 pi.on 包装,不算丢失 + warnings.push(`事件 ${eventName} → ${mapping.pi}(${mapping.note})—— ⚠️ 桥接仅 pi/DSH 运行时有效;mcode CLI 不加载 pi extension,事件无消费者(2026-08-15 实证)`); + } else { + warnings.push(`事件 ${eventName} → ${mapping.pi}(${mapping.note};当前生成 stub,需人工移植业务逻辑)`); + } + } + + if (tools.length === 0 && events.length === 0) { + warnings.push('未识别到任何 tools.register / registerTool / ctx.on 调用,请人工检查源插件形态'); + } + + // ── 若提取到工具:同时生成 agent-plugins MCP 包(MCode /plugins 可直接安装)── + let mcpPluginName; + let mcpToolCount = 0; + if (tools.length > 0) { + // 优先用源 plugin 自带的 name 字段,其次用源文件名(去 dsh- 前缀) + mcpPluginName = makePluginName(sourcePluginName || base.replace(/^dsh-/, '') || base, base); + const serverName = sanitizeToolName(mcpPluginName) || 'tools'; + writeFileSafe(path.join(outDir, 'plugin.json'), JSON.stringify({ + $schema: 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json', + name: mcpPluginName, + version: '0.1.0', + description: `由 DSH 插件「${sourcePluginName || base}」转换:提供 ${tools.length} 个 MCP 工具(${tools.map((t) => t.name).join('、')})。`, + author: { name: 'crossplug' }, + keywords: ['dsh', 'converted', 'mcp'], + extensions: { crossplug: { source: srcFile, sourceKind: 'dsh-plugin-tools', sourceName: sourcePluginName || undefined, generatedAt: new Date().toISOString() } }, + }, null, 2) + '\n'); + writeFileSafe(path.join(outDir, 'mcp.json'), JSON.stringify({ + $schema: 'https://agent-plugins.org/schemas/1.0.0/mcp.schema.json', + mcpServers: { + [serverName]: { type: 'stdio', command: 'node', args: ['./mcp-server.js'] }, + }, + }, null, 2) + '\n'); + // vendor/dsh-plugin.js 已经在下面 copyFileSync 写过了;mcp-server.js 用 import + 模拟 ctx 加载它 + writeFileSafe(path.join(outDir, 'mcp-server.js'), renderToolsMcpServer(tools, serverName, sourcePluginName || base)); + mcpToolCount = tools.length; + } + + // ── 生成 extension.js ── + // 关键变化:tools 不再"内联 execute 字符串",而是 vendor 加载原插件 + 模拟 ctx,让 apply() 内部注册的工具被收集,然后逐个 pi.registerTool 派发。 + // 闭包(cfg / recall / formatRecall 等)保留在原 apply 作用域里,工具行为真实。 + const lines = []; + lines.push('// 由 DSH 插件源码转换生成 — 源: ' + srcFile); + lines.push('// 生成时间: ' + new Date().toISOString()); + lines.push('// 转换器: crossplug (core/dsh2mcode.js, plugin-source 模式)'); + lines.push('//'); + lines.push('// 工具执行走 vendor/dsh-plugin.js 加载 + 模拟 ctx 路径,'); + lines.push('// 闭包(cfg / recall / formatRecall 等)保留在原 apply() 作用域里,行为真实。'); + lines.push(''); + lines.push('import dshPlugin from "./vendor/dsh-plugin.js";'); + lines.push('import { Type } from "./typebox-shim.js";'); + lines.push(''); + lines.push('// DSH 工具注册的元数据缓存(mockCtx.tools.register 时写入)'); + // 镜像块:与 renderToolsMcpServer 模板中的 mockTools/mockCtx 保持一致(改动需两侧同步) + lines.push('const mockTools = {'); + lines.push(' registered: new Map(),'); + lines.push(' register(spec) {'); + lines.push(' this.registered.set(spec.name, spec);'); + lines.push(' return () => this.registered.delete(spec.name);'); + lines.push(' },'); + lines.push('};'); + lines.push(''); + lines.push('// 模拟 DSH ctx:tools 真注册;on() 记录 handler(不丢弃),供 before_agent_start 等桥接 wrapper 调'); + lines.push('const mockCtx = {'); + lines.push(' get(name) {'); + lines.push(' if (name === "tools") return mockTools;'); + lines.push(' if (name === "agents") return { currentInitiator: () => null };'); + lines.push(' return undefined;'); + lines.push(' },'); + lines.push(' _listeners: {},'); + lines.push(' on(event, handler) {'); + lines.push(' (this._listeners[event] || (this._listeners[event] = [])).push(handler);'); + lines.push(' },'); + lines.push(' effect(fn) { try { fn(); } catch {} },'); + lines.push('}'); + lines.push(''); + lines.push('// 触发原 DSH 插件 apply,populate mockTools'); + lines.push('try {'); + lines.push(' // ESM 命名空间对象:真实默认导出在 .default;要兼容:default 是函数 / default 是 { apply } / 直接是函数 / 直接是 { apply }'); + lines.push(' const target = (dshPlugin && dshPlugin.default) ? dshPlugin.default : dshPlugin;'); + lines.push(' if (typeof target === "function") target(mockCtx, {});'); + lines.push(' else if (target && typeof target.apply === "function") target.apply(mockCtx, {});'); + lines.push(' else throw new Error("vendor 没有 default 导出且无 apply 方法(既不是函数也不是带 apply 的对象)");'); + lines.push('} catch (err) {'); + lines.push(' console.error("[crossplug] vendor apply 抛错: " + (err && err.message ? err.message : err));'); + lines.push('}'); + lines.push(''); + lines.push('export default function (pi) {'); + lines.push(''); + + for (const t of tools) { + let typeboxCode = 'Type.Any()'; + let schemaNote = ''; + const norm = normalizeDshSchema(t.parametersText); + if (norm) { + try { + typeboxCode = jsonSchemaToTypeboxCode(norm); + } catch { + schemaNote = ' // 参数 schema 规范化后仍无法生成 typebox,已用 Type.Any() 兜底'; + } + } else { + schemaNote = ' // 参数 schema 无法解析为 JS 字面量/JSON,已用 Type.Any() 兜底'; + } + lines.push(` // ── 工具 ${t.name} ──`); + if (schemaNote) lines.push(schemaNote); + lines.push(` pi.registerTool({`); + lines.push(` name: ${JSON.stringify(t.name.replace(/[^A-Za-z0-9_]/g, '_'))},`); + lines.push(` label: ${JSON.stringify(t.name)},`); + lines.push(` description: ${JSON.stringify(t.description || '(无描述)')},`); + lines.push(` parameters: ${typeboxCode},`); + lines.push(` async execute(toolCallId, params, _signal, _onUpdate, _ctx) {`); + lines.push(` // 从 mockTools 拿原 DSH 工具 spec(vendor apply 已注册),派发到原 execute(args)`); + lines.push(` const spec = mockTools.registered.get(${JSON.stringify(t.name)});`); + lines.push(` if (!spec || typeof spec.execute !== "function") {`); + lines.push(` return { content: [{ type: "text", text: ${JSON.stringify('[crossplug] 原工具 ' + t.name + ' 在 vendor 加载阶段未注册到 tools')} }], details: {} };`); + lines.push(` }`); + lines.push(` try {`); + lines.push(` // 第二参 exec 上下文(callId/signal)传给原工具,保持 DSH 工具契约`); + lines.push(` const raw = await spec.execute(params, { callId: toolCallId, signal: _signal });`); + lines.push(` // 适配 DSH 的 output.render(如有)或直接走 text 通道`); + lines.push(` if (spec.output && typeof spec.output.render === "function") {`); + lines.push(` const rendered = spec.output.render(params, raw);`); + lines.push(` if (Array.isArray(rendered)) return { content: rendered, details: { raw } };`); + lines.push(` }`); + lines.push(` if (raw && typeof raw === "object" && Array.isArray(raw.content)) return { content: raw.content, details: raw.details || {} };`); + lines.push(` const text = raw && typeof raw === "object" && typeof raw.text === "string" ? raw.text : (typeof raw === "string" ? raw : JSON.stringify(raw, null, 2));`); + lines.push(` return { content: [{ type: "text", text }], details: { raw } };`); + lines.push(` } catch (err) {`); + lines.push(` return { content: [{ type: "text", text: ${JSON.stringify('[crossplug] ' + t.name + ' 执行失败: ') + ' + (err && err.message ? err.message : String(err))'} }], details: {} };`); + lines.push(` }`); + lines.push(` },`); + lines.push(` });`); + lines.push(''); + } + + for (const ev of events) { + const mapped = EVENT_MAP[ev.event]; + if (mapped && mapped.bridge) { + if (mapped.bridgeKind === 'turn-end') { + // 桥接 pi turn_end → DSH session/event(turn/end 子事件)。 + // DSH handler 签名 (session, event),按 event.type 过滤;payload 在 event.data。 + // 注:mockSession 是占位(无真实消息),deriveMessages 返回空 —— 需要完整消息的 + // handler 需人工适配(见 doc/event-mapping-questions.md)。 + lines.push(` // ── 事件 ${ev.event}(turn/end) → pi.${mapped.pi}(桥接:把 pi turn_end 转成 DSH turn/end 子事件)──`); + lines.push(` pi.on(${JSON.stringify(mapped.pi)}, async (event, _ctx) => {`); + lines.push(` const handlers = (mockCtx._listeners && mockCtx._listeners[${JSON.stringify(ev.event)}]) || [];`); + lines.push(` if (!handlers.length) return;`); + lines.push(` // DSH session/event handler 收到 (session, event),按 event.type === "turn/end" 过滤`); + lines.push(` // Q2 答复:pi turn_end 事件含 message(AgentMessage),传给 DSH handler 的 session.deriveMessages()`); + lines.push(` const mockSession = { id: "bridged", deriveMessages: () => {`); + lines.push(` const m = event && event.message;`); + lines.push(` if (m === undefined || m === null) return [];`); + lines.push(` if (Array.isArray(m)) return m;`); + lines.push(` return [typeof m === "string" ? { role: "assistant", content: [{ type: "text", text: m }] } : m];`); + lines.push(` } };`); + lines.push(` // Q2 答复:pi turn_end 事件含 turnIndex(轮次序号),传给 DSH turn/end 的 data.turn`); + lines.push(` const dshEvent = { type: "turn/end", seq: 0, time: Date.now(), data: { turn: (event && event.turnIndex !== undefined) ? event.turnIndex : 0, reason: { kind: "completed" } } };`); + lines.push(` for (const handler of handlers) {`); + lines.push(` try {`); + lines.push(` await handler(mockSession, dshEvent);`); + lines.push(` } catch (err) {`); + lines.push(` console.error("[crossplug] " + ${JSON.stringify(ev.event)} + " handler 抛错: " + (err && err.message ? err.message : err));`); + lines.push(` }`); + lines.push(` }`); + lines.push(` });`); + lines.push(''); + } else { + // bridgeKind === 'assembly':桥接 DSH (assembly, context, next) → pi before_agent_start。 + // DSH handler 往 assembly.sections.push({name, text});pi 返回 { systemPrompt } 注入增补。 + lines.push(` // ── 事件 ${ev.event} → pi.${mapped.pi}(桥接:调原 DSH handler)──`); + lines.push(` pi.on(${JSON.stringify(mapped.pi)}, async (event, _ctx) => {`); + lines.push(` const handlers = (mockCtx._listeners && mockCtx._listeners[${JSON.stringify(ev.event)}]) || [];`); + lines.push(` if (!handlers.length) return;`); + lines.push(` // 模拟 DSH 的 assembly:原 handler 会往 assembly.sections.push({name, text})`); + lines.push(` const assembly = { sections: [] };`); + lines.push(` const fakeContext = { scope: { _marker: true } };`); + lines.push(` for (const handler of handlers) {`); + lines.push(` try {`); + lines.push(` await handler(assembly, fakeContext, () => {});`); + lines.push(` } catch (err) {`); + lines.push(` console.error("[crossplug] " + ${JSON.stringify(ev.event)} + " handler 抛错: " + (err && err.message ? err.message : err));`); + lines.push(` }`); + lines.push(` }`); + lines.push(` if (!assembly.sections.length) return;`); + lines.push(` // 把 DSH 注入的 sections 合并成 system prompt 增补`); + lines.push(` const injected = assembly.sections.map((s) => s.text || "").join("\\n\\n");`); + lines.push(` const base = (event && typeof event.systemPrompt === "string") ? event.systemPrompt : "";`); + lines.push(` return { systemPrompt: base + (base ? "\\n\\n" : "") + injected };`); + lines.push(` });`); + lines.push(''); + } + } else if (mapped) { + // 有映射但不可桥接:保留 stub + lines.push(` // ── 事件 ${ev.event} → pi.${mapped.pi}(${mapped.note};语义不直通,需人工移植)──`); + lines.push(` pi.on(${JSON.stringify(mapped.pi)}, async (_event, _ctx) => {`); + lines.push(` // 原 DSH 处理逻辑需人工移植(事件语义不 1:1)`); + lines.push(` });`); + lines.push(''); + } else { + lines.push(` // ⚠ 事件 ${ev.event} 无 mcode/pi 对应,已跳过(能力在转换后丢失)`); + lines.push(''); + } + } + + if (!tools.length && !events.length) { + lines.push(` // ⚠ 未在源码中识别到 tools.register 或 ctx.on 调用。`); + lines.push(` // 原插件源码完整保留在 vendor/ 目录,请人工移植。`); + lines.push(''); + } + + lines.push('}'); + lines.push(''); + + const pkgName = `dsh-plugin-${slugify(sourcePluginName || base)}-bridge`; + writeFileSafe(path.join(outDir, 'extension.js'), lines.join('\n')); + writeFileSafe(path.join(outDir, 'typebox-shim.js'), typeboxShimCode()); + writeFileSafe(path.join(outDir, 'package.json'), JSON.stringify({ + name: pkgName, + version: '0.1.0', + description: `由 DSH 插件源码 ${path.basename(srcFile)} 转换生成的 pi extension`, + type: 'module', + main: 'extension.js', + dependencies: { typebox: '*' }, + dshToMcode: { source: srcFile, sourceKind: 'dsh-plugin-source', generatedAt: new Date().toISOString() }, + }, null, 2) + '\n'); + // 保留原源码作 vendor 加载源 + fs.mkdirSync(path.join(outDir, 'vendor'), { recursive: true }); + // 用固定名 dsh-plugin.js 以便 mcp-server.js / extension.js 可以稳定 import + fs.copyFileSync(srcFile, path.join(outDir, 'vendor', 'dsh-plugin.js')); + writeFileSafe(path.join(outDir, 'CONVERSION-REPORT.md'), [ + `# 转换报告:DSH 插件源码 → mcode 插件包`, + '', + `- 来源: ${srcFile}`, + `- 源 plugin name: ${sourcePluginName || '(未识别,使用文件名 ' + base + ')'}`, + `- 生成时间: ${new Date().toISOString()}`, + '', + `## 工具(${tools.length})`, + '', + ...tools.map((t) => `- \`${t.name}\`(execute 走 vendor 模拟 ctx 调用,闭包保留)${t.description ? ':' + t.description : ''}`), + '', + `## 事件(${events.length})`, + '', + ...events.map((e) => `- \`${e.event}\`${EVENT_MAP[e.event] ? ' → pi.' + EVENT_MAP[e.event].pi : ' → ❌ 丢失(无 mcode/pi 对应)'}${e.event === 'session/event' ? '(仅 turn/end 子事件桥接;turn/start、tool/call、tool/result、user/message、assistant/message 等子事件不桥接)' : ''}`), + '', + `## 丢失能力`, + '', + ...(lostCapabilities.length + ? lostCapabilities.map((c) => `- ${c}`) + : ['(无)']), + '', + `## 产物`, + '', + ...(mcpPluginName + ? [`- agent-plugins MCP 包:\`plugin.json\` + \`mcp.json\` + \`mcp-server.js\`(${mcpToolCount} 个 MCP 工具)→ 复制到 ~/.minimax/plugins/${mcpPluginName}/,MCode /plugins → Local 安装`] + : []), + [`- vendor:原 DSH 插件源码 \`vendor/dsh-plugin.js\`(被 mcp-server.js / extension.js 加载)`], + [`- pi extension:\`extension.js\` + \`typebox-shim.js\` —— ⚠️ 仅独立 pi CLI 生效;mcode CLI 不加载 pi extension(2026-08-15 实证,mcode-plugin-spec.md §1.1);mcode 侧生效载体是上面的 agent-plugins MCP 包`], + '', + `## 警告`, + '', + ...(warnings.length ? warnings.map((w) => `- ${w}`) : ['(无)']), + '', + ].join('\n') + '\n'); + + return { + kind: 'plugin-source', + source: srcFile, + out: outDir, + packageName: pkgName, + mcpPluginName: mcpPluginName || undefined, + mcpTools: mcpToolCount, + tools: tools.length, + events: events.length, + warnings: warnings.length, + reportFile: path.join(outDir, 'CONVERSION-REPORT.md'), + }; +} + +// 工具注册型插件 → MCP stdio 服务器(newline JSON-RPC) +// vendor 加载 + 模拟 ctx:原 DSH 插件的 execute 闭包完整保留(cfg / recall / formatRecall 等不变) +function renderToolsMcpServer(tools, serverName, sourceBase) { + // MCP 广播名(sanitized)→ vendor 注册的原始工具名:MCP 客户端按广播名调用, + // 派发时必须还原原始名查 mockTools.registered(原名可含 - 等 MCP 广播不允许的字符)。 + const nameMap = {}; + const toolDefs = tools.map((t) => { + let inputSchema = { type: 'object', properties: {} }; + const norm = normalizeDshSchema(t.parametersText); + if (norm && typeof norm === 'object') inputSchema = norm; + const safeName = String(t.name).replace(/[^A-Za-z0-9_]/g, '_'); + nameMap[safeName] = t.name; + return { + name: safeName, + description: t.description || `由 DSH 插件「${sourceBase}」转换的工具(crossplug 桥接)`, + inputSchema, + }; + }); + + // 不再内联 execute 字符串;用 mockTools.registered 派发到 vendor 加载的原 spec + return `#!/usr/bin/env node +// 由 crossplug 生成:把 DSH 插件(${sourceBase})的工具注册桥接为 MCP stdio 服务器。 +// 协议:newline-delimited JSON-RPC 2.0(initialize / tools/list / tools/call / ping / notifications/*)。 +// 执行路径:vendor/dsh-plugin.js → mockCtx.tools.register → mockTools.registered → 本文件派发。 +// 闭包(cfg / recall / formatRecall 等)保留在原 apply() 作用域里。 +import { createInterface } from "node:readline"; +import { pathToFileURL } from "node:url"; +import { dirname, resolve as resolvePath } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +// 加载原 DSH 插件(ESM) +const dshPlugin = await import(pathToFileURL(resolvePath(__dirname, "./vendor/dsh-plugin.js")).href); + +// 镜像块:与 convertPluginSource 生成的 extension.js 中的 mockTools/mockCtx 保持一致(改动需两侧同步) +// DSH 工具注册表(apply() 触发后填充) +const mockTools = { + registered: new Map(), + register(spec) { + this.registered.set(spec.name, spec); + return () => this.registered.delete(spec.name); + }, +}; + +// 模拟 DSH ctx:tools 真注册;on() 记录 handler(不丢弃),供桥接 wrapper 调用 +const mockCtx = { + get(name) { + if (name === "tools") return mockTools; + if (name === "agents") return { currentInitiator: () => null }; + return undefined; + }, + _listeners: {}, + on(event, handler) { + (this._listeners[event] || (this._listeners[event] = [])).push(handler); + }, + effect(fn) { try { fn(); } catch {} }, +}; + +// 触发原 DSH 插件 apply +// ESM 命名空间对象:真实默认导出在 .default;要兼容:default 是函数 / default 是 { apply } / 直接是函数 / 直接是 { apply } +try { + const target = (dshPlugin && dshPlugin.default) ? dshPlugin.default : dshPlugin; + if (typeof target === "function") target(mockCtx, {}); + else if (target && typeof target.apply === "function") target.apply(mockCtx, {}); + else throw new Error("vendor 没有 default 导出且无 apply 方法(既不是函数也不是带 apply 的对象)"); +} catch (err) { + process.stderr.write("[crossplug] vendor apply 抛错: " + (err && err.message ? err.message : err) + "\\n"); +} + +const TOOLS = ${JSON.stringify(toolDefs, null, 2)}; + +// 广播名 → 原始注册名(名字含 - 等字符的工具经此还原) +const NAME_MAP = ${JSON.stringify(nameMap, null, 2)}; + +function respond(id, result) { + process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id, result }) + "\\n"); +} +function respondError(id, code, message) { + process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } }) + "\\n"); +} + +const rl = createInterface({ input: process.stdin, crlfDelay: Infinity }); +rl.on("line", async (line) => { + if (!line.trim()) return; + let msg; + try { + msg = JSON.parse(line); + } catch { + return; + } + const { id, method, params } = msg; + try { + if (method === "initialize") { + respond(id, { + protocolVersion: (params && params.protocolVersion) || "2024-11-05", + capabilities: { tools: {} }, + serverInfo: { name: "crossplug-${serverName}", version: "1.0.0" }, + }); + } else if (method === "notifications/initialized" || method === "notifications/cancelled") { + // 通知类:无需回复 + } else if (method === "ping") { + respond(id, {}); + } else if (method === "tools/list") { + respond(id, { tools: TOOLS }); + } else if (method === "tools/call") { + const name = params && params.name; + const args = (params && params.arguments) || {}; + const originalName = NAME_MAP[name] || name; + const spec = mockTools.registered.get(originalName); + if (!spec || typeof spec.execute !== "function") { + return respondError(id, -32602, "unknown tool or tool not registered by vendor: " + name); + } + try { + // 第二参 exec 上下文:MCP 无 callId/signal,传 undefined 保持 DSH 工具契约(工具内可选链安全) + const raw = await spec.execute(args, { callId: undefined, signal: undefined }); + let text; + if (spec.output && typeof spec.output.render === "function") { + const rendered = spec.output.render(args, raw); + if (Array.isArray(rendered)) { + text = rendered.map((c) => (c && typeof c.text === "string" ? c.text : JSON.stringify(c))).join("\\n"); + } else { + text = JSON.stringify(rendered); + } + } else if (raw && typeof raw === "object" && typeof raw.text === "string") { + text = raw.text; + } else if (typeof raw === "string") { + text = raw; + } else { + text = JSON.stringify(raw, null, 2); + } + respond(id, { content: [{ type: "text", text: text || "(空结果)" }] }); + } catch (err) { + respondError(id, -32603, "[crossplug] tool execute failed: " + (err && err.message ? err.message : String(err))); + } + } else { + respondError(id, -32601, "method not found: " + method); + } + } catch (e) { + respondError(id, -32603, String(e && e.message ? e.message : e)); + } +}); +`; +} + +// 向上查找源插件的 package.json,取其 name +function findPluginPackageName(srcFile) { + let dir = path.dirname(srcFile); + for (let i = 0; i < 3; i++) { + const pkg = path.join(dir, 'package.json'); + if (fs.existsSync(pkg)) { + try { + const parsed = JSON.parse(fs.readFileSync(pkg, 'utf8')); + if (parsed && typeof parsed.name === 'string' && parsed.name.trim()) return parsed.name.trim(); + } catch { /* 忽略坏 package.json */ } + } + const parent = path.dirname(dir); + if (parent === dir) break; + dir = parent; + } + return undefined; +} + +// MCP 工具名:小写下划线,去掉 dsh-/web-search/-provider 等词缀 +function sanitizeToolName(name) { + return String(name) + .toLowerCase() + .replace(/^dsh-/, '') + .replace(/^web-?search-/, '') + .replace(/-web-?search/, '') + .replace(/-provider$/, '') + .replace(/[^a-z0-9]+/g, '_') + .replace(/^_+|_+$/g, '') || 'search'; +} + + +function indentCode(code, pad) { + return code + .split('\n') + .map((l) => (l === '' ? '' : ' '.repeat(pad) + l)) + .join('\n'); +} + +// 找第一个不在字符串/注释中的 '{'(用于多参数调用中定位对象字面量) +function firstTopLevelBrace(text) { + let state = 'code'; + for (let i = 0; i < text.length; i++) { + const c = text[i]; + const n = text[i + 1]; + if (state === 'str-single') { + if (c === '\\') i++; + else if (c === "'") state = 'code'; + continue; + } + if (state === 'str-double') { + if (c === '\\') i++; + else if (c === '"') state = 'code'; + continue; + } + if (state === 'line-comment') { + if (c === '\n') state = 'code'; + continue; + } + if (state === 'block-comment') { + if (c === '*' && n === '/') { state = 'code'; i++; } + continue; + } + if (c === "'") { state = 'str-single'; continue; } + if (c === '"') { state = 'str-double'; continue; } + if (c === '/' && n === '/') { state = 'line-comment'; i++; continue; } + if (c === '/' && n === '*') { state = 'block-comment'; i++; continue; } + if (c === '{') return i; + } + return -1; +} + +// ═══ 模式三:DSH 搜索提供方插件(registerSearchProvider)→ agent-plugins MCP 包 ═══ +// +// 把 ctx.web.registerSearchProvider(...) 型插件转换为 MCode 可安装的 +// agent-plugins.org 1.0.0 插件包:原插件 vendor 进 vendor/(@deepseek-ai 依赖 +// 重写为本地垫片),生成 mcp-server.js(stdio JSON-RPC)把 provider.search() +// 暴露为 MCP 工具 searxng_search,mcp.json 声明 MCP 服务器。 + +// @deepseek-ai 依赖 → 本地垫片文件名映射 +const DSH_IMPORT_SHIMS = { + '@deepseek-ai/schemastery': 'schemastery-shim.js', + '@deepseek-ai/dsh-web': 'dsh-web-shim.js', + '@deepseek-ai/dsh-launch-environment': 'launch-env-shim.js', +}; + +function shimSourceFor(pkg) { + if (pkg === '@deepseek-ai/schemastery') { + return [ + '// 由 crossplug 生成的 schemastery 最小垫片:链式方法全部空操作,', + '// 只满足 Config 声明在模块顶层的求值(运行时校验由宿主负责)。', + 'function chainable(target) {', + ' return new Proxy(target || {}, {', + ' get(t, prop) {', + ' if (prop in t) return t[prop];', + ' if (typeof prop === "symbol") return undefined;', + ' return () => chainable(t);', + ' },', + ' });', + '}', + 'const z = {', + ' object: (shape) => chainable({ type: "object", shape }),', + ' string: () => chainable({ type: "string" }),', + ' number: () => chainable({ type: "number" }),', + ' integer: () => chainable({ type: "integer" }),', + ' boolean: () => chainable({ type: "boolean" }),', + ' array: (item) => chainable({ type: "array", item }),', + ' optional: (v) => v,', + ' union: (v) => chainable({ type: "union", anyOf: v }),', + ' literal: (v) => chainable({ type: "literal", const: v }),', + '};', + 'export default z;', + '', + ].join('\n'); + } + if (pkg === '@deepseek-ai/dsh-web') { + return [ + '// 由 crossplug 生成的 dsh-web 最小垫片:WebError 保持错误码语义。', + 'export class WebError extends Error {', + ' constructor(message, code, options) {', + ' super(message);', + ' this.name = "WebError";', + ' this.code = code;', + ' if (options && options.cause !== undefined) this.cause = options.cause;', + ' }', + '}', + '', + ].join('\n'); + } + if (pkg === '@deepseek-ai/dsh-launch-environment') { + return [ + '// 由 crossplug 生成的 launch-environment 垫片:从 process.env 读取受管变量。', + 'export function launchEnvironmentOf() {', + ' return {', + ' get(name) {', + ' const value = process.env[name] || "";', + ' return value ? { value } : undefined;', + ' },', + ' };', + '}', + '', + ].join('\n'); + } + return undefined; +} + +// 重写 vendor 源码:@deepseek-ai 依赖指向本地垫片;其他 @deepseek-ai/@earendil-works 导入注释掉 +function rewriteProviderImports(source, warnings) { + let out = source; + const used = new Set(); + const lineRe = /^(\s*import\s+[^\n]*?from\s+["'])(@deepseek-ai\/[^"']+|@earendil-works\/[^"']+)(["'][^\n]*)$/gm; + out = out.replace(lineRe, (whole, head, pkg, tail) => { + if (DSH_IMPORT_SHIMS[pkg]) { + used.add(pkg); + return `${head}./${DSH_IMPORT_SHIMS[pkg]}${tail}`; + } + warnings.push(`移除了无法解析的运行时导入 ${pkg}(桥接环境不提供,相关逻辑需人工核对)`); + return `// [crossplug] removed import: ${whole.trim().slice(0, 120)}`; + }); + // 裸导入(无 from):import "@deepseek-ai/xxx" + out = out.replace(/^(\s*import\s+["'])(@deepseek-ai\/[^"']+|@earendil-works\/[^"']+)(["'][^\n]*)$/gm, (whole, head, pkg, tail) => { + warnings.push(`移除了无法解析的裸导入 ${pkg}`); + return `// [crossplug] removed bare import: ${whole.trim().slice(0, 120)}`; + }); + return { source: out, used }; +} + +function renderMcpServer(toolName, description) { + return `#!/usr/bin/env node +// 由 crossplug 生成:把 DSH 搜索提供方插件(registerSearchProvider)包装为 MCP stdio 服务器。 +// 协议:newline-delimited JSON-RPC 2.0(initialize / tools/list / tools/call / ping)。 +// SearXNG 地址从环境变量 SEARXNG_URL 读取(由 mcp.json 的 env 注入)。 +import { createInterface } from "node:readline"; +import { apply } from "./vendor/index.js"; + +let provider = null; +apply( + { + web: { + registerSearchProvider(p) { + provider = p; + }, + }, + }, + { + url: process.env.SEARXNG_URL || "", + maxResults: 30, + categories: "", + language: "", + timeoutMs: 20000, + }, +); + +const TOOLS = [ + { + name: ${JSON.stringify(toolName)}, + description: ${JSON.stringify(description)}, + inputSchema: { + type: "object", + properties: { + query: { type: "string", description: "搜索查询" }, + categories: { type: "string", description: "逗号分隔的分类(如 news,it)" }, + language: { type: "string", description: "语言提示;缺省自动检测" }, + maxResults: { type: "integer", description: "最大结果数(缺省 30)" }, + }, + required: ["query"], + }, + }, +]; + +function respond(id, result) { + process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id, result }) + "\\n"); +} +function respondError(id, code, message) { + process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } }) + "\\n"); +} + +const rl = createInterface({ input: process.stdin, crlfDelay: Infinity }); +rl.on("line", async (line) => { + if (!line.trim()) return; + let msg; + try { + msg = JSON.parse(line); + } catch { + return; + } + const { id, method, params } = msg; + try { + if (method === "initialize") { + respond(id, { + protocolVersion: (params && params.protocolVersion) || "2024-11-05", + capabilities: { tools: {} }, + serverInfo: { name: "crossplug-searxng", version: "1.0.0" }, + }); + } else if (method === "notifications/initialized" || method === "notifications/cancelled") { + // 通知类:无需回复 + } else if (method === "ping") { + respond(id, {}); + } else if (method === "tools/list") { + respond(id, { tools: TOOLS }); + } else if (method === "tools/call") { + const name = params && params.name; + const args = (params && params.arguments) || {}; + if (name !== TOOLS[0].name) return respondError(id, -32602, "unknown tool: " + name); + if (!provider) return respondError(id, -32603, "SearXNG provider 未就绪:请设置 SEARXNG_URL(mcp.json env 或系统环境变量)"); + const signal = new AbortController().signal; + const result = await provider.search({ query: args.query }, signal); + const text = (result.sources || []) + .map((s) => "- " + (s.title || s.url) + "\\n " + s.url + (s.snippet ? "\\n " + s.snippet : "")) + .join("\\n"); + respond(id, { content: [{ type: "text", text: text || "(无结果)" }] }); + } else { + respondError(id, -32601, "method not found: " + method); + } + } catch (e) { + respondError(id, -32603, String(e && e.message ? e.message : e)); + } +}); +`; +} + +function convertProviderPlugin(srcFile, outDir) { + const source = fs.readFileSync(srcFile, 'utf8'); + const warnings = []; + const base = path.basename(srcFile, path.extname(srcFile)); + // 插件真实名字:优先从源插件包 package.json 提取(srcFile 可能在 lib/ 子目录) + const pkgName = findPluginPackageName(srcFile); + const pluginName = makePluginName(pkgName || base, base); + const toolName = sanitizeToolName(pluginName) + '_search'; + const pkgDir = outDir; + + // ── vendor:原插件 + import 重写 + 垫片 ── + const rewritten = rewriteProviderImports(source, warnings); + fs.mkdirSync(path.join(pkgDir, 'vendor'), { recursive: true }); + writeFileSafe(path.join(pkgDir, 'vendor', 'index.js'), rewritten.source); + for (const pkg of Object.keys(DSH_IMPORT_SHIMS)) { + if (rewritten.used.has(pkg)) { + writeFileSafe(path.join(pkgDir, 'vendor', DSH_IMPORT_SHIMS[pkg]), shimSourceFor(pkg)); + } + } + + // ── MCP 服务器 ── + const description = `通过 SearXNG 实例搜索网页(由 DSH 插件「${pluginName}」转换,crossplug 桥接)。`; + writeFileSafe(path.join(pkgDir, 'mcp-server.js'), renderMcpServer(toolName, description)); + + // ── mcp.json(agent-plugins 1.0.0)── + const searxngUrl = process.env.SEARXNG_URL || ''; + const mcp = { + $schema: 'https://agent-plugins.org/schemas/1.0.0/mcp.schema.json', + mcpServers: { + [toolName]: { + type: 'stdio', + command: 'node', + args: ['./mcp-server.js'], + env: { SEARXNG_URL: searxngUrl }, + }, + }, + }; + writeFileSafe(path.join(pkgDir, 'mcp.json'), JSON.stringify(mcp, null, 2) + '\n'); + if (!searxngUrl) { + warnings.push('本机未设置 SEARXNG_URL:请在 mcp.json 的 env 或系统环境变量中配置 SearXNG 实例地址'); + } + + // ── plugin.json(agent-plugins 1.0.0)── + writeFileSafe(path.join(pkgDir, 'plugin.json'), JSON.stringify({ + $schema: 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json', + name: pluginName, + version: '1.0.0', + description: `由 DSH 插件「${pluginName}」转换:提供 SearXNG 网页搜索(MCP 工具 ${toolName})。`, + author: { name: 'crossplug' }, + keywords: ['dsh', 'converted', 'search', 'searxng'], + extensions: { + crossplug: { source: srcFile, sourceKind: 'dsh-search-provider', generatedAt: new Date().toISOString() }, + }, + }, null, 2) + '\n'); + + // ── 说明 skill + 报告 + README ── + writeFileSafe(path.join(pkgDir, 'skills', 'search-notes', 'SKILL.md'), [ + '---', + 'name: search-notes', + 'description: 本插件由 DSH 搜索提供方插件「' + base + '」转换而来:提供 SearXNG 网页搜索(MCP 工具 ' + toolName + ')。', + '---', + '', + '本插件把 DSH 的 SearXNG 搜索提供方桥接为 MCP 工具。', + '原插件逻辑(vendor/index.js)零移植复用,通过 mcp.json 声明的 stdio MCP 服务器暴露。', + '使用前确认 SearXNG 实例可达:mcp.json 的 env.SEARXNG_URL(或系统环境变量)。', + '调用示例:' + toolName + ' 工具,参数 query="DeepSeek Harness"', + '', + ].join('\n') + '\n'); + + writeFileSafe(path.join(pkgDir, 'CONVERSION-REPORT.md'), [ + `# 转换报告:DSH 搜索提供方插件 → agent-plugins MCP 包`, + '', + `- 来源: ${srcFile}`, + `- 插件名: ${pluginName}`, + `- 生成时间: ${new Date().toISOString()}`, + `- 转换方式: 运行时桥接(vendor + 依赖垫片)`, + '', + `## 能力映射`, + '', + `- \`ctx.web.registerSearchProvider(...)\` → MCP 工具 \`${toolName}\`(mcp.json 声明,stdio 服务器 mcp-server.js)`, + `- 依赖 @deepseek-ai/schemastery、dsh-web、dsh-launch-environment → vendor/ 下本地垫片`, + `- 原搜索逻辑(语言检测 / categories / 归一化)→ 原样复用`, + '', + `## 安装`, + '', + `1. 复制本目录到 ~/.minimax/plugins/${pluginName}/`, + `2. MiniMax Code 中 /plugins → Local → 安装并启用`, + `3. 确认 SearXNG 地址:mcp.json env.SEARXNG_URL 或系统环境变量`, + '', + `## 警告`, + '', + ...(warnings.length ? warnings.map((w) => `- ${w}`) : ['(无)']), + '', + ].join('\n') + '\n'); + + writeFileSafe(path.join(pkgDir, 'README.md'), [ + `# ${pluginName}`, + '', + `由 crossplug 从 DSH 插件「${base}」转换(运行时桥接为 MCP 服务器)。`, + '', + `- MCP 工具:\`${toolName}\`(stdio,node ./mcp-server.js)`, + `- 配置:\`mcp.json\` 的 \`env.SEARXNG_URL\`,或系统环境变量`, + `- 原插件源码:\`vendor/index.js\`(依赖已垫片化)`, + '', + ].join('\n') + '\n'); + + return { + kind: 'provider-bridge', + source: srcFile, + out: pkgDir, + packageName: pluginName, + toolName, + warnings: warnings.length, + reportFile: path.join(pkgDir, 'CONVERSION-REPORT.md'), + }; +} + +// ═══ 入口 ═══ + +function convert(src, outDir) { + const stat = fs.statSync(src); + if (stat.isDirectory()) return convertPreset(src, outDir); + if (stat.isFile()) { + const source = fs.readFileSync(src, 'utf8'); + // 搜索提供方插件(ctx.web.registerSearchProvider):转为 agent-plugins MCP 包 + if (/registerSearchProvider\s*\(/.test(source)) return convertProviderPlugin(src, outDir); + return convertPluginSource(src, outDir); + } + throw new Error(`无法识别的输入: ${src}`); +} + +module.exports = { convert, convertPreset, convertPluginSource, convertProviderPlugin }; diff --git a/plugins/Fectivnfy112357/crossplug/core/extract.js b/plugins/Fectivnfy112357/crossplug/core/extract.js new file mode 100644 index 0000000..7cf00d6 --- /dev/null +++ b/plugins/Fectivnfy112357/crossplug/core/extract.js @@ -0,0 +1,281 @@ +// extract.js — JS/TS 源码中的调用与对象字面量提取(零依赖) +// +// 用于: +// - dsh2mcode:从 DSH 插件源码提取 registerTool/defineTool/ctx.on 调用 +// - mcode2dsh:从 pi extension 源码提取 registerTool/registerCommand 调用 +// 实现:词法级扫描(字符串/注释/括号配对),不做完整 JS 解析。 + +'use strict'; + +// 找到所有匹配正则的"调用"并截取平衡括号内的参数文本 +// namePattern: RegExp,匹配函数名(不含 '('),可带捕获组 1 作为"名字"(如事件名) +function findCalls(source, namePattern) { + const re = new RegExp(namePattern.source, namePattern.flags.includes('g') ? namePattern.flags : namePattern.flags + 'g'); + const out = []; + let m; + while ((m = re.exec(source)) !== null) { + // 跳过作为字符串一部分的匹配:检查前一个非空白字符 + let k = m.index - 1; + while (k >= 0 && /\s/.test(source[k])) k--; + if (k >= 0 && (source[k] === '"' || source[k] === "'" || source[k] === '`')) continue; + // 扫描到下一个 '(' + let i = m.index + m[0].length; + while (i < source.length && /\s/.test(source[i])) i++; + if (source[i] !== '(') continue; + const close = matchParen(source, i); + if (close < 0) continue; + out.push({ + start: m.index, + end: close + 1, + argsStart: i + 1, + argsEnd: close, + argsText: source.slice(i + 1, close), + name: m[1] !== undefined ? m[1] : m[0], + }); + re.lastIndex = close + 1; + } + return out; +} + +// 从 pos 开始的 '(' 找到配对的 ')'(支持嵌套与字符串/注释) +function matchParen(source, pos) { + const stack = []; + let i = pos; + let state = 'code'; // code | str-single | str-double | str-template | line-comment | block-comment + while (i < source.length) { + const c = source[i]; + const n = source[i + 1]; + if (state === 'line-comment') { + if (c === '\n') state = 'code'; + i++; + continue; + } + if (state === 'block-comment') { + if (c === '*' && n === '/') { + state = 'code'; + i += 2; + } else i++; + continue; + } + if (state === 'str-single') { + if (c === '\\') i += 2; + else if (c === "'") state = 'code', i++; + else i++; + continue; + } + if (state === 'str-double') { + if (c === '\\') i += 2; + else if (c === '"') state = 'code', i++; + else i++; + continue; + } + if (state === 'str-template') { + if (c === '\\') i += 2; + else if (c === '`') state = 'code', i++; + else if (c === '$' && n === '{') { + // 模板插值:进入代码态直到匹配的 } + i += 2; + const close = matchParen(source, i - 1); + if (close < 0) return -1; + i = close + 1; + } else i++; + continue; + } + // code 态 + if (c === '/' && n === '/') { state = 'line-comment'; i += 2; continue; } + if (c === '/' && n === '*') { state = 'block-comment'; i += 2; continue; } + if (c === "'") { state = 'str-single'; i++; continue; } + if (c === '"') { state = 'str-double'; i++; continue; } + if (c === '`') { state = 'str-template'; i++; continue; } + if (c === '(' || c === '{' || c === '[') { stack.push(c === '(' ? ')' : c === '{' ? '}' : ']'); i++; continue; } + if (c === ')' || c === '}' || c === ']') { + const want = stack.pop(); + if (want === undefined) return -1; + if (want !== c) return -1; + if (stack.length === 0) return i; + i++; + continue; + } + i++; + } + return -1; +} + +// 从对象字面量文本中提取某个属性的值(平衡截取;字符串返回解引号后的值) +function propertyValue(objText, key) { + const re = new RegExp('(?:^|[,{\\s])' + key + '\\s*:'); + const m = re.exec(objText); + if (!m) return undefined; + let i = m.index + m[0].length; + while (i < objText.length && /\s/.test(objText[i])) i++; + if (i >= objText.length) return undefined; + const c = objText[i]; + if (c === '"' || c === "'" || c === '`') { + // 字符串值:读到匹配的结束引号(简单处理转义) + const quote = c; + let j = i + 1; + let out = ''; + while (j < objText.length) { + const ch = objText[j]; + if (ch === '\\') { out += objText[j + 1] ?? ''; j += 2; continue; } + if (ch === quote) break; + out += ch; + j++; + } + return { kind: 'string', value: out, start: i, end: j + 1 }; + } + if (c === '(' || c === '{' || c === '[') { + const close = matchParen(objText, i); + if (close < 0) return undefined; + return { kind: 'code', value: objText.slice(i, close + 1), start: i, end: close + 1 }; + } + // 标量/标识符:读到逗号或行尾(在对象文本内) + let j = i; + let depth = 0; + while (j < objText.length) { + const ch = objText[j]; + if (ch === '(' || ch === '{' || ch === '[') depth++; + if ((ch === ',' || ch === '}') && depth === 0) break; + if (ch === ')' || ch === '}' || ch === ']') depth--; + j++; + } + return { kind: 'code', value: objText.slice(i, j).trim(), start: i, end: j }; +} + +// 提取函数体(含 async)的源码文本:从 'function'/'=>' 起始位置截取完整函数 +// 传入的值文本可能形如 `async function (a, b) { ... }` 或 `(a, b) => { ... }` 或 `async (a,b) => {...}` +function functionText(raw) { + const s = raw.trim(); + const brace = s.indexOf('{'); + if (brace < 0) return undefined; + const close = matchParen(s, brace); + if (close < 0) return undefined; + return s.slice(0, close + 1); +} + +// 提取对象字面量中的方法简写:`async execute(args) { ... }` / `execute(args) { ... }` +// 返回 { kind: 'code', value: 'execute(args) { ... }', start, end } 或 undefined +function propertyMethod(objText, key) { + const re = new RegExp('(?:^|[,\\s])(async\\s+)?' + key + '\\s*\\('); + const m = re.exec(objText); + if (!m) return undefined; + const paramsStart = m.index + m[0].length - 1; // '(' 位置 + const paramsClose = matchParen(objText, paramsStart); + if (paramsClose < 0) return undefined; + let i = paramsClose + 1; + while (i < objText.length && /\s/.test(objText[i])) i++; + if (objText[i] !== '{') return undefined; + const bodyClose = matchParen(objText, i); + if (bodyClose < 0) return undefined; + const start = m.index + (m[1] ? m[1].length : 0); // 从 key 开始(不含 async 前缀?保留 async) + return { + kind: 'code', + value: objText.slice(start, bodyClose + 1), + start, + end: bodyClose + 1, + }; +} + +// 求值 JS 对象字面量子集(单/双引号字符串、数字、布尔、null、undefined、数组、对象) +// 用于把 DSH 参数 schema(ParameterSchemaSpec)从源码文本解析为可用的 JSON。 +function jsLiteralToValue(text) { + const s = text.trim(); + if (s === '') return undefined; + const p = new JsLiteralParser(s); + const v = p.parseValue(); + if (!p.atEnd()) throw new Error('jsLiteral: 意外的尾随内容 @' + p.pos); + return v; +} + +class JsLiteralParser { + constructor(text) { + this.t = text; + this.pos = 0; + } + atEnd() { + while (this.pos < this.t.length && /\s/.test(this.t[this.pos])) this.pos++; + return this.pos >= this.t.length; + } + peek() { + while (this.pos < this.t.length && /\s/.test(this.t[this.pos])) this.pos++; + return this.t[this.pos]; + } + parseValue() { + const c = this.peek(); + if (c === '{') return this.parseObject(); + if (c === '[') return this.parseArray(); + if (c === "'" || c === '"') return this.parseString(); + if (c === 'u' && this.t.startsWith('undefined', this.pos)) { this.pos += 9; return undefined; } + if (c === 'n' && this.t.startsWith('null', this.pos)) { this.pos += 4; return null; } + if (c === 't' && this.t.startsWith('true', this.pos)) { this.pos += 4; return true; } + if (c === 'f' && this.t.startsWith('false', this.pos)) { this.pos += 5; return false; } + const m = /^-?\d+(\.\d+)?([eE][+-]?\d+)?/.exec(this.t.slice(this.pos)); + if (m) { this.pos += m[0].length; return Number(m[0]); } + // 无法识别的表达式(变量、函数等) + throw new Error('jsLiteral: 无法解析 @' + this.pos + ': ' + this.t.slice(this.pos, this.pos + 30)); + } + parseString() { + const quote = this.t[this.pos]; + this.pos++; + let out = ''; + while (this.pos < this.t.length) { + const c = this.t[this.pos]; + if (c === '\\') { + const n = this.t[this.pos + 1]; + if (n === 'n') out += '\n'; + else if (n === 't') out += '\t'; + else if (n === 'r') out += '\r'; + else if (n === 'u') { + out += String.fromCharCode(parseInt(this.t.slice(this.pos + 2, this.pos + 6), 16)); + this.pos += 4; + } else out += n; + this.pos += 2; + continue; + } + if (c === quote) { this.pos++; return out; } + out += c; + this.pos++; + } + throw new Error('jsLiteral: 字符串未闭合'); + } + parseArray() { + this.pos++; // [ + const out = []; + while (true) { + const c = this.peek(); + if (c === ']') { this.pos++; return out; } + out.push(this.parseValue()); + const c2 = this.peek(); + if (c2 === ',') { this.pos++; continue; } + if (c2 === ']') { this.pos++; return out; } + throw new Error('jsLiteral: 数组语法错误 @' + this.pos); + } + } + parseObject() { + this.pos++; // { + const out = {}; + while (true) { + const c = this.peek(); + if (c === '}') { this.pos++; return out; } + // key: 标识符或字符串 + let key; + if (c === "'" || c === '"') key = this.parseString(); + else { + const m = /^[A-Za-z_$][\w$]*/.exec(this.t.slice(this.pos)); + if (!m) throw new Error('jsLiteral: 对象键错误 @' + this.pos); + key = m[0]; + this.pos += m[0].length; + } + while (this.pos < this.t.length && /\s/.test(this.t[this.pos])) this.pos++; + if (this.t[this.pos] !== ':') throw new Error('jsLiteral: 缺冒号 @' + this.pos); + this.pos++; + out[key] = this.parseValue(); + const c2 = this.peek(); + if (c2 === ',') { this.pos++; continue; } + if (c2 === '}') { this.pos++; return out; } + throw new Error('jsLiteral: 对象语法错误 @' + this.pos); + } + } +} + +module.exports = { findCalls, matchParen, propertyValue, propertyMethod, functionText, jsLiteralToValue }; diff --git a/plugins/Fectivnfy112357/crossplug/core/install.js b/plugins/Fectivnfy112357/crossplug/core/install.js new file mode 100644 index 0000000..26f4dd5 --- /dev/null +++ b/plugins/Fectivnfy112357/crossplug/core/install.js @@ -0,0 +1,342 @@ +// install.js — 把转换器适配器安装到 DSH 或 mcode(零依赖) + +'use strict'; + +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); + +const REPO_ROOT = path.resolve(__dirname, '..'); +const ADAPTERS_DIR = path.join(REPO_ROOT, 'adapters'); +const CORE_DIR = __dirname; + +function dshHome() { + return process.env.DSH_HOME || path.join(os.homedir(), '.dsh'); +} +function minimaxHome() { + return path.join(os.homedir(), '.minimax'); +} +function piHome() { + return path.join(os.homedir(), '.pi'); +} + +function copyDir(src, dst) { + fs.mkdirSync(dst, { recursive: true }); + for (const entry of fs.readdirSync(src, { withFileTypes: true })) { + const s = path.join(src, entry.name); + const d = path.join(dst, entry.name); + if (entry.isDirectory()) copyDir(s, d); + else if (entry.isFile()) fs.copyFileSync(s, d); + } +} + +// ── 安装到 DSH:~/.dsh/.agent-presets// ── + +function installDsh(opts = {}) { + const presetId = opts.presetId || 'crossplug'; + const target = path.join(dshHome(), '.agent-presets', presetId); + if (fs.existsSync(target) && !opts.force) { + throw new Error(`目标已存在(使用 --force 覆盖): ${target}`); + } + const srcPreset = path.join(ADAPTERS_DIR, 'dsh'); + if (!fs.existsSync(path.join(srcPreset, 'preset.yml'))) { + throw new Error(`适配器缺失: ${path.join(srcPreset, 'preset.yml')}`); + } + fs.rmSync(target, { recursive: true, force: true }); + copyDir(srcPreset, target); + copyDir(CORE_DIR, path.join(target, 'core')); + return { + ok: true, + side: 'dsh', + presetId, + target, + files: ['preset.yml', 'agent.cordis.yml', 'plugins/convert.js', 'core/'], + next: '重启 DSH 会话或在启动器中选择该 preset;或在本会话中直接使用转换工具。', + }; +} + +// ── 安装到 mcode:agent-plugins 插件(~/.minimax/plugins/crossplug/)+ pi extension ── +// +// 两种入口: +// A) 不传 srcDir:把 adapters/mcode/ 装为 crossplug 自身(默认) +// B) 传 srcDir:把 dsh2mcode 转换产物(已是 agent-plugins 1.0.0 格式)整目录装到 +// ~/.minimax/plugins//,name 从 srcDir/plugin.json 读 + +function readPluginNameFromDir(srcDir) { + const pj = path.join(srcDir, 'plugin.json'); + if (!fs.existsSync(pj)) return null; + try { + const obj = JSON.parse(fs.readFileSync(pj, 'utf8')); + return obj && typeof obj.name === 'string' ? obj.name : null; + } catch { return null; } +} + +function installMcode(opts = {}) { + const srcDir = opts.srcDir; // 转换产物目录(新) + const isFromConvert = !!(srcDir && fs.existsSync(srcDir)); + const extName = opts.extName || (isFromConvert ? (readPluginNameFromDir(srcDir) || 'converted-plugin') : 'crossplug'); + const srcExt = isFromConvert ? srcDir : path.join(ADAPTERS_DIR, 'mcode'); + // extension.js 仅对 crossplug 自身适配器必需;转换产物(含社区结构包: + // plugin.json + README + LICENSE + skills/,无 extension.js)允许缺失,pi 端安装自动跳过。 + if (!isFromConvert && !fs.existsSync(path.join(srcExt, 'extension.js'))) { + throw new Error(`适配器缺失: ${path.join(srcExt, 'extension.js')}`); + } + const installed = []; + + // 1) agent-plugins 格式插件(MCode /plugins 的 Local 标签页可见,可安装/启用) + const pluginDir = opts.pluginDir || path.join(minimaxHome(), 'plugins', extName); + if (fs.existsSync(pluginDir) && !opts.force) { + throw new Error(`MCode 插件目录已存在(使用 --force 覆盖): ${pluginDir}`); + } + fs.rmSync(pluginDir, { recursive: true, force: true }); + fs.mkdirSync(pluginDir, { recursive: true }); + if (isFromConvert) { + // 转换产物整目录复制(plugin.json / mcp.json / mcp-server.js / skills/ / vendor/ 都带上) + copyDir(srcExt, pluginDir); + // core/ 不是 mcp 运行时需要的,转换产物里通常没有,无需额外补 + } else { + // 老路径:adapters/mcode/ → crossplug 自身 + for (const f of ['plugin.json', 'extension.js']) { + const s = path.join(srcExt, f); + if (fs.existsSync(s)) fs.copyFileSync(s, path.join(pluginDir, f)); + } + copyDir(path.join(srcExt, 'skills'), path.join(pluginDir, 'skills')); + copyDir(CORE_DIR, path.join(pluginDir, 'core')); + } + installed.push(`插件包: ${pluginDir}(/plugins → Local 可见)`); + + // 2) pi extension + // - 非 srcDir 路径:装 crossplug 自身到 pi(老行为) + // - srcDir 路径:转换产物的 extension.js 含事件桥接(如 before_agent_start), + // 自动注入系统提示词的能力靠 pi 端跑出来;这里也把 extension.js 装到 pi 端 + 写入 settings.json + let extDir = null; + if (!isFromConvert) { + extDir = opts.extDir || path.join(piHome(), 'agent', 'extensions', extName); + fs.mkdirSync(path.dirname(extDir), { recursive: true }); + if (fs.existsSync(extDir) && !opts.force) { + throw new Error(`pi 扩展目录已存在(使用 --force 覆盖): ${extDir}`); + } + fs.rmSync(extDir, { recursive: true, force: true }); + copyDir(srcExt, extDir); + copyDir(CORE_DIR, path.join(extDir, 'core')); + fs.rmSync(path.join(extDir, 'plugin.json'), { force: true }); // pi 侧不需要 manifest + fs.rmSync(path.join(extDir, 'skills'), { recursive: true, force: true }); + installed.push(`pi extension: ${extDir}`); + } else { + // srcDir 路径:装到 ~/.pi/agent/extensions//(只放 extension.js + vendor/,不需要 plugin.json / mcp.json / skills/) + const srcExtJs = path.join(srcExt, 'extension.js'); + if (fs.existsSync(srcExtJs)) { + extDir = opts.extDir || path.join(piHome(), 'agent', 'extensions', extName); + fs.mkdirSync(path.dirname(extDir), { recursive: true }); + if (fs.existsSync(extDir) && !opts.force) { + // 已经在;保留现有,跳过 + installed.push(`pi extension: 已存在 ${extDir}(保留;想重装用 --force)`); + } else { + fs.rmSync(extDir, { recursive: true, force: true }); + fs.mkdirSync(extDir, { recursive: true }); + // extension.js 自身 + fs.copyFileSync(srcExtJs, path.join(extDir, 'extension.js')); + // vendor/(被 extension.js import) + const srcVendor = path.join(srcExt, 'vendor'); + if (fs.existsSync(srcVendor)) { + copyDir(srcVendor, path.join(extDir, 'vendor')); + } + // typebox-shim.js(被 extension.js import) + const srcTypebox = path.join(srcExt, 'typebox-shim.js'); + if (fs.existsSync(srcTypebox)) { + fs.copyFileSync(srcTypebox, path.join(extDir, 'typebox-shim.js')); + } + installed.push(`pi extension: ${extDir}(含 before_agent_start 等事件桥接)`); + } + } else { + installed.push(`pi extension: 转换产物无 extension.js,跳过 pi 端安装`); + } + } + + // ⚠️ 2026-08-15 实证更正(mcode-plugin-spec.md §1.1/§2.1):mcode CLI 不加载 pi extension, + // ~/.minimax/settings.json 的 extensions 数组是死配置(cli.js 无消费逻辑)。 + // 不再默认写入;仅当显式 --pi-settings 时写入(目标运行时是独立 pi CLI 的场景)。 + const mmSettings = path.join(minimaxHome(), 'settings.json'); + if (extDir && opts.piSettings) { + let settings = {}; + if (fs.existsSync(mmSettings)) { + try { settings = JSON.parse(fs.readFileSync(mmSettings, 'utf8')); } catch { settings = {}; } + } + const extFile = path.join(extDir, 'extension.js'); + const extList = Array.isArray(settings.extensions) ? settings.extensions : []; + if (!extList.includes(extFile)) extList.push(extFile); + settings.extensions = extList; + fs.mkdirSync(minimaxHome(), { recursive: true }); + fs.writeFileSync(mmSettings, JSON.stringify(settings, null, 2) + '\n', 'utf8'); + installed.push(`settings: ${mmSettings}(--pi-settings 显式开启;mcode CLI 不消费此数组)`); + } else if (extDir) { + installed.push(`pi extension 已装到 ${extDir}(仅独立 pi CLI 生效;mcode CLI 不加载 pi extension,2026-08-15 实证)`); + } + + return { + ok: true, + side: 'mcode', + extName, + extDir, + pluginDir, + srcDir: isFromConvert ? srcDir : undefined, + settingsFile: extDir ? mmSettings : undefined, + files: installed, + next: isFromConvert + ? `在 MiniMax Code 中运行 \`mcode plugin enable ${extName}@local\`(或 /plugins UI)即可使用` + : '在 MiniMax Code 中打开 /plugins → Local 标签页,安装 crossplug 插件(Enter),Ctrl+R 刷新;pi 侧命令 /convert-dsh 与 /convert-mcode 需重启或 /reload。', + }; +} + +// ── 安装到 DSH 宿主组合:~/.dsh/profiles//plugins/dsh-crossplug/ ── +// +// 与 installDsh(装 agent preset)不同,这是把转换桥作为**宿主组合插件** +// 安装:插件随 DSH 进程启动常驻,所有会话可用,不依赖预设。 +// 安装源:adapters/dsh-host/(lib/index.js + package.json + README.md) +// + 项目 core/(转换引擎)→ 目标 plugins/dsh-crossplug/。 +// 挂载:在 profile 的 cordis.patch.yml 追加 insert 行(幂等)。 + +const HOST_PLUGIN_ID = 'dsh-crossplug'; +const HOST_PATCH_BLOCK = + '\n' + + '# ── DSH ↔ mcode 插件转换桥 (dsh-crossplug) ─────────────────────────────────\n' + + '# 宿主组合插件:convert_plugin / convert_list 工具常驻,所有会话可用,\n' + + '# 不依赖任何 agent preset。由 crossplug 项目安装:node core/run.js install --side dsh-host\n' + + '- insert:\n' + + ' - id: dsh-crossplug\n' + + " name: './plugins/dsh-crossplug/lib/index.js'\n"; + +function installDshHost(opts = {}) { + const profile = opts.profile || 'web'; + const profileDir = path.join(dshHome(), 'profiles', profile); + if (!fs.existsSync(profileDir)) { + throw new Error(`DSH profile 不存在: ${profileDir}(可选 --profile )`); + } + const src = path.join(ADAPTERS_DIR, 'dsh-host'); + if (!fs.existsSync(path.join(src, 'lib', 'index.js'))) { + throw new Error(`宿主插件适配器缺失: ${path.join(src, 'lib', 'index.js')}`); + } + const target = path.join(profileDir, 'plugins', HOST_PLUGIN_ID); + if (fs.existsSync(target) && !opts.force) { + throw new Error(`目标已存在(使用 --force 覆盖): ${target}`); + } + + fs.rmSync(target, { recursive: true, force: true }); + fs.mkdirSync(target, { recursive: true }); + for (const f of ['package.json', 'README.md']) { + const s = path.join(src, f); + if (fs.existsSync(s)) fs.copyFileSync(s, path.join(target, f)); + } + copyDir(path.join(src, 'lib'), path.join(target, 'lib')); + copyDir(CORE_DIR, path.join(target, 'core')); + // 插件主体是 ESM("type": "module"),core/ 是 CommonJS:就近声明隔离。 + fs.writeFileSync( + path.join(target, 'core', 'package.json'), + '{\n "name": "dsh-crossplug-core",\n "private": true,\n "type": "commonjs"\n}\n', + 'utf8', + ); + + // patch 层:追加挂载行(幂等) + const patchFile = path.join(profileDir, 'cordis.patch.yml'); + let patchText = ''; + if (fs.existsSync(patchFile)) patchText = fs.readFileSync(patchFile, 'utf8'); + let patchAction = 'already-present'; + if (!patchText.includes(HOST_PLUGIN_ID)) { + const header = patchText.length > 0 && !patchText.endsWith('\n') ? '\n' : ''; + fs.appendFileSync(patchFile, header + HOST_PATCH_BLOCK, 'utf8'); + patchAction = 'appended'; + } + + return { + ok: true, + side: 'dsh-host', + profile, + target, + patchFile, + patchAction, + files: ['package.json', 'lib/index.js', 'core/', 'core/package.json', 'README.md'], + next: '重启 DSH 进程后生效:所有会话获得 convert_plugin / convert_list 工具(插件列表可见 dsh-crossplug)。', + }; +} + +// 把插件包内的 skills/ 注册为 MiniMax Code 用户技能(~/.minimax/skills//), +// 使 /技能名 作为斜杠命令可用(MCode 会把 skill 命令作为消息交给 agent 执行)。 +function registerPluginSkills(pluginDir) { + const skillsSrc = path.join(pluginDir, 'skills'); + if (!fs.existsSync(skillsSrc)) return { registered: [], skipped: [] }; + const userSkills = path.join(minimaxHome(), 'skills'); + fs.mkdirSync(userSkills, { recursive: true }); + const registered = []; + const skipped = []; + for (const child of fs.readdirSync(skillsSrc, { withFileTypes: true })) { + if (!child.isDirectory()) continue; + const name = String(child.name).toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '') || child.name; + const target = path.join(userSkills, name); + if (fs.existsSync(target)) { + skipped.push(name); + continue; + } + copyDir(path.join(skillsSrc, child.name), target); + registered.push(name); + } + return { registered, skipped }; +} + +// ── 安装转换产物(host 模式)为 DSH 宿主组合插件 ── +// srcDir 是 mcode2dsh --host 的输出(lib/index.js + vendor/ + package.json)。 +function installDshHostPlugin(srcDir, opts = {}) { + const profile = opts.profile || 'web'; + const profileDir = path.join(dshHome(), 'profiles', profile); + if (!fs.existsSync(profileDir)) { + throw new Error(`DSH profile 不存在: ${profileDir}(可选 --profile )`); + } + const entry = path.join(srcDir, 'lib', 'index.js'); + if (!fs.existsSync(entry)) { + throw new Error(`srcDir 不是 host 模式转换产物(缺 lib/index.js): ${srcDir}(请用 node core/run.js mcode2dsh --out --host)`); + } + // 插件名:package.json 的 name(转换器已写为 presetId) + let name = path.basename(srcDir); + try { + const pkg = JSON.parse(fs.readFileSync(path.join(srcDir, 'package.json'), 'utf8')); + if (pkg && typeof pkg.name === 'string' && pkg.name.trim()) name = pkg.name.trim(); + } catch { /* 忽略 */ } + const target = path.join(profileDir, 'plugins', name); + if (fs.existsSync(target) && !opts.force) { + throw new Error(`目标已存在(使用 --force 覆盖): ${target}`); + } + fs.rmSync(target, { recursive: true, force: true }); + fs.mkdirSync(target, { recursive: true }); + copyDir(srcDir, target); + // core/ 的 CJS 隔离不需要(转换产物 lib/vendor 均为 ESM) + + // patch 层:追加 insert 行(幂等) + const patchFile = path.join(profileDir, 'cordis.patch.yml'); + let patchText = ''; + if (fs.existsSync(patchFile)) patchText = fs.readFileSync(patchFile, 'utf8'); + let patchAction = 'already-present'; + const block = + '\n' + + '# ── mcode 转换插件: ' + name + ' (crossplug mcode2dsh --host) ─────────────────────\n' + + '# 宿主组合插件:所有会话可用,不依赖 agent preset。\n' + + '- insert:\n' + + ' - id: ' + name + '\n' + + " name: './plugins/" + name + "/lib/index.js'\n"; + if (!patchText.includes("id: " + name)) { + const header = patchText.length > 0 && !patchText.endsWith('\n') ? '\n' : ''; + fs.appendFileSync(patchFile, header + block, 'utf8'); + patchAction = 'appended'; + } + return { + ok: true, + side: 'dsh-host', + profile, + pluginName: name, + target, + patchFile, + patchAction, + files: ['lib/index.js', 'vendor/', 'package.json', 'README.md'], + next: '重启 DSH 进程后生效:所有会话获得该插件的工具/命令(' + name + ')。', + }; +} + +module.exports = { installDsh, installDshHost, installDshHostPlugin, installMcode, registerPluginSkills, dshHome, minimaxHome, piHome }; diff --git a/plugins/Fectivnfy112357/crossplug/core/mapping.js b/plugins/Fectivnfy112357/crossplug/core/mapping.js new file mode 100644 index 0000000..365bda9 --- /dev/null +++ b/plugins/Fectivnfy112357/crossplug/core/mapping.js @@ -0,0 +1,82 @@ +// mapping.js — 已知 DSH 插件行 ↔ pi/mcode 能力映射表(零依赖) +// +// 用途:dsh2mcode 把 DSH agent preset 的每一行翻译成 pi extension 里 +// 可表达的能力,或标记为"无对应能力"的桩。 +// +// kind 取值: +// builtin — pi 运行时自带该能力,无需转换(extension 中只写注释) +// tool — 生成 pi.registerTool 桩(同名、同描述、空参数) +// command — 生成 pi.registerCommand 桩 +// persona — 生成 /persona 命令与启动通知 +// note — 只写说明注释(无运行时动作) +// warn — 无对应能力,生成带警告的桩 + +'use strict'; + +// 已知 @deepseek-ai/dsh-* 插件行的映射 +const ROW_MAP = { + persona: { kind: 'persona', pi: '无直接对应:DSH 的 persona 是系统提示注入,pi 无等价 API;转换后通过 /persona 命令与启动通知保留', note: 'persona 文本会完整写入 extension 注释与 CONVERSION-REPORT.md' }, + 'agent-instructions': { kind: 'note', pi: 'agent-instructions 是指令大小限制,无 pi 对应', note: '' }, + + // ── shell / 文件 ── + 'tool-bash': { kind: 'builtin', pi: 'pi 自带 bash 工具', note: '' }, + 'tool-pwsh': { kind: 'builtin', pi: 'pi 自带 bash 工具;PowerShell 语义需人工核对', note: '' }, + 'tool-fs': { kind: 'builtin', pi: 'pi 自带 read/write/edit 文件工具', note: '' }, + 'tool-fs-search': { kind: 'builtin', pi: 'pi 自带 grep/glob 搜索能力', note: '' }, + + // ── 任务 / 后台 / 目标 ── + 'tool-todo': { kind: 'tool', pi: 'pi 无内建任务清单工具;生成同名桩,逻辑需移植(pi 可用 appendEntry + details 自建持久状态)', note: 'execute 为占位实现' }, + 'tool-jobs': { kind: 'warn', pi: 'pi 无后台任务注册表对应', note: '' }, + 'tool-goal': { kind: 'warn', pi: 'pi 无 goal 服务对应;会话树与 setLabel 可作轻量替代,持续目标由模型自行跟进', note: '' }, + + // ── 网络 ── + 'tool-web': { kind: 'builtin', pi: 'pi 自带网页搜索/抓取能力(如配置了 web 搜索)', note: '' }, + 'tool-web-search': { kind: 'builtin', pi: 'pi 自带网页搜索能力', note: '' }, + + // ── 交互 ── + 'tool-ask-user': { kind: 'tool', pi: 'pi 用 ctx.ui.confirm/input 表达;生成桩工具并注释对应 ctx.ui 用法', note: '建议改写成 ctx.ui 交互' }, + + // ── 委派 / 工作流 ── + // pi 官方 examples/extensions/ 有 subagent/(registerTool + exec)与 plan-mode/ + // 完整实现——以下能力不是"pi 没有",而是"pi 用扩展自行实现"。 + 'tool-subagent': { kind: 'warn', pi: 'pi 无内建子代理工具;但扩展可自行实现(pi 官方示例 examples/extensions/subagent/:registerTool + exec)', note: '' }, + 'tool-subagent-control': { kind: 'warn', pi: '无直接对应;扩展自建子代理时可用 pi.getAllTools() 自省 + setActiveTools 控制', note: '' }, + 'tool-subagent-list-agents': { kind: 'warn', pi: '无直接对应;pi.getAllTools() 返回全部工具元数据(含 sourceInfo)可作自省', note: '' }, + 'tool-workflow': { kind: 'warn', pi: '无 workflow 引擎内建;可用动态工具加载(setActiveTools 纯增量)模拟按需能力', note: '' }, + 'tool-ralph': { kind: 'warn', pi: '无 fresh-agent 循环内建;扩展可自行实现(registerTool + sendUserMessage 接力)', note: '' }, + + // ── Cordis 自指 ── + 'tool-cordis': { kind: 'warn', pi: '无对应:pi 没有运行时内省工具', note: '' }, + + // ── 规划 / 压缩 / 委派分组(组合概念)── + planning: { kind: 'note', pi: 'pi 无内建 plan mode;但扩展可实现(pi 官方示例 examples/extensions/plan-mode/:registerCommand + registerShortcut + registerFlag + setActiveTools)', note: '' }, + compaction: { kind: 'note', pi: '上下文压缩由 pi 自带管理(session_before_compact / session_compact 事件可自定义)', note: '' }, + delegation: { kind: 'note', pi: '子代理/工作流无内建,可经扩展实现(见 tool-subagent 一行)', note: '' }, + + // ── 命令 ── + 'command-compact': { kind: 'command', pi: '生成 /compact 桩命令', note: 'pi 自带压缩;桩仅提示' }, + 'command-plan': { kind: 'command', pi: '生成 /plan 桩命令', note: 'pi 无计划模式;桩仅提示' }, + + // ── 技能 ── + 'skill-filesystem': { kind: 'note', pi: '技能已通过 ~/.agents/skills 共享,无需转换', note: '' }, + 'tool-skill': { kind: 'note', pi: '技能加载机制双方独立;技能内容走共享目录', note: '' }, +}; + +// 兜底:未知 @deepseek-ai/dsh-* 行 +function classifyRow(row) { + const name = row.name || ''; + const base = name.replace(/^@deepseek-ai\//, ''); + // npm 包名形如 dsh-tool-ask-user;映射表键是 tool-ask-user —— 两种都试 + const short = base.replace(/^dsh-/, ''); + // 组行(cordis:group)与本地文件行:按行 id 匹配(如 planning / compaction / delegation) + const idShort = String(row.id || '').replace(/^dsh-/, ''); + if (ROW_MAP[base]) return ROW_MAP[base]; + if (ROW_MAP[short]) return ROW_MAP[short]; + if (ROW_MAP[idShort]) return ROW_MAP[idShort]; + if (base.startsWith('dsh-')) { + return { kind: 'warn', pi: '未知 DSH 插件行,无自动映射;请人工评估', note: '' }; + } + return { kind: 'warn', pi: '非 @deepseek-ai 插件行(可能是本地文件或第三方包),无自动映射', note: '' }; +} + +module.exports = { ROW_MAP, classifyRow }; diff --git a/plugins/Fectivnfy112357/crossplug/core/mcode2dsh.js b/plugins/Fectivnfy112357/crossplug/core/mcode2dsh.js new file mode 100644 index 0000000..26c94b4 --- /dev/null +++ b/plugins/Fectivnfy112357/crossplug/core/mcode2dsh.js @@ -0,0 +1,702 @@ +// mcode2dsh.js — mcode(MiniMax Code / pi extension)→ DSH agent preset(零依赖) +// +// 输入: +// 1. pi extension 文件(.js / .ts) +// 2. mcode 插件包目录(package.json 的 main / index.js / extension.js) +// +// 输出:一个自包含的 DSH agent preset: +// preset.yml — 元数据 +// agent.cordis.yml — persona 行 + 桥接插件行(name: ./plugins/bridge.js) +// plugins/bridge.js — 运行时桥接:用 pi-API 垫片调用原 extension 的工厂, +// registerTool → ctx.tools.register,registerCommand → ctx.commands.register +// vendor/ — 原 extension(TS 已剥离类型;typebox 导入重写为本地垫片) +// CONVERSION-REPORT.md — 提取到的工具/命令清单与警告 +// +// 桥接方案让原插件的 execute/命令逻辑原样运行,无需转译逻辑本身。 + +'use strict'; + +const fs = require('node:fs'); +const path = require('node:path'); +const { spawnSync } = require('node:child_process'); +const { findCalls } = require('./extract.js'); +const { INLINE_BRIDGE_HELPERS, typeboxShimCode } = require('./schema.js'); + +const EXT_NAMES = ['index.js', 'extension.js', 'main.js', 'index.ts', 'extension.ts', 'main.ts']; + +// ── 定位 extension 文件 ── + +function locateExtension(dir) { + const pkgPath = path.join(dir, 'package.json'); + if (fs.existsSync(pkgPath)) { + try { + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + if (pkg.main) { + const main = path.resolve(dir, pkg.main); + if (fs.existsSync(main)) return { file: main, pkg }; + } + // pi 包格式:package.json 的 "pi": { "extensions": ["./src/index.ts"] } 声明扩展入口 + // (见 pi 官方 docs/extensions.md "Package with dependencies") + if (pkg.pi && Array.isArray(pkg.pi.extensions)) { + for (const entry of pkg.pi.extensions) { + if (typeof entry !== 'string') continue; + const f = path.resolve(dir, entry); + if (fs.existsSync(f)) return { file: f, pkg }; + } + } + } catch { /* 忽略坏 package.json */ } + } + for (const name of EXT_NAMES) { + const f = path.join(dir, name); + if (fs.existsSync(f)) return { file: f, pkg: undefined }; + } + return undefined; +} + +// ── TS 类型剥离(保守版)── + +function stripTypes(source) { + let out = source; + // 1) 整行类型导入/导出 + out = out.replace(/^\s*import\s+type\b[^\n]*$/gm, ''); + out = out.replace(/^\s*export\s+type\b[^\n]*$/gm, ''); + // 2) interface / type 声明(单行或多行到匹配的 '}')—— 用括号配对处理多行 + out = out.replace(/^\s*(export\s+)?interface\s+\w+[^{]*\{[\s\S]*?\n\}/gm, ''); + out = out.replace(/^\s*(export\s+)?type\s+\w+\s*=[^;]*;/gm, ''); + // 3) 常见类型标注 `: Type` / `: string` 等 + out = out.replace(/:\s*(Type\.\w+|string|number|boolean|unknown|any|void|never|object|Array<[^>]*>|Promise<[^>]*>|Record]*>|ExtensionAPI|ExtensionContext|ExtensionCommandContext|JsonValue|StringEnum<[^>]*>)\b/g, ''); + // 4) `as X` 断言(保留 const 等 enum 场景?as const 也要去) + out = out.replace(/\s+as\s+(const|Type|string|number|boolean|unknown|any|never|JsonValue|ExtensionAPI|ExtensionContext)\b/g, ''); + // 5) 泛型实参 <...>(保守:只处理紧随标识符且内容是类型词汇的) + out = out.replace(/([A-Za-z_$][\w$]*)\s*<([A-Za-z_$][\w$]*|Type\.\w+|string|number|boolean|unknown|any|void)(\s*,\s*[^>]+)?>\s*(?=[(=;{])/g, '$1'); + return out; +} + +function checkSyntax(file) { + const r = spawnSync(process.execPath, ['--check', file], { encoding: 'utf8' }); + return r.status === 0 ? { ok: true } : { ok: false, error: (r.stderr || '').trim() }; +} + +// ── typebox 导入重写 ── + +function rewriteVendorSource(source, baseName, warnings) { + let out = source; + // 1) 去掉 pi 包的 import type 行 + out = out.replace(/^\s*import\s+type[^\n]*from\s+["']@earendil-works[^\n]*$/gm, ''); + out = out.replace(/^\s*import[^\n]*from\s+["']@earendil-works\/pi-(?:coding-agent|ai|tui|agent-core)[^\n]*$/gm, (m) => { + warnings.push('移除了 pi 包的运行时导入(桥接环境不提供):' + m.trim().slice(0, 80)); + return ''; + }); + // 2) typebox 导入 → 本地垫片 + out = out.replace(/from\s+["']typebox["']/g, 'from "./typebox-shim.js"'); + if (/from\s+["']typebox["']/.test(source)) warnings.push('typebox 导入已重写为本地垫片 vendor/typebox-shim.js'); + // 3) vendor 相对导入修正:extension 平铺进输出 vendor/ 后,原 "./vendor/X"(相对 + // 源包目录)会解析成 vendor/vendor/X —— 重写为 "./X"(X 已随 vendor/ 一起复制)。 + out = out.replace(/from\s+["']\.\/vendor\/([^"']+)["']/g, 'from "./$1"'); + if (/from\s+["']\.\/vendor\//.test(source)) warnings.push('源包的 ./vendor/ 相对导入已修正为平铺路径'); + return out; +} + +// ── 生成 ── + +function convert(extensionFile, outDir, opts = {}) { + const isHost = !!opts.host; // host 模式:输出宿主组合插件(lib/index.js),而非 agent preset + const src = path.resolve(extensionFile); + const ext = path.extname(src); + const isTs = ext === '.ts'; + const source = fs.readFileSync(src, 'utf8'); + const warnings = []; + + // 包元数据(若输入是包目录) + let pkgMeta = undefined; + const srcDir = path.dirname(src); + const pkgPath = path.join(srcDir, 'package.json'); + if (fs.existsSync(pkgPath)) { + try { pkgMeta = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); } catch { /* 忽略 */ } + } + + // 提取注册信息(用于报告;运行时由桥接器兜底) + const tools = []; + const commands = []; + const events = []; + for (const call of findCalls(source, /(?:pi\.)?registerTool/g)) { + const m = /name:\s*["']([^"']+)["']/.exec(call.argsText); + if (m) tools.push(m[1]); + } + for (const call of findCalls(source, /(?:pi\.)?registerCommand/g)) { + const m = /^\s*["']([^"']+)["']/.exec(call.argsText); + if (m) commands.push(m[1]); + } + for (const call of findCalls(source, /(?:pi\.)?on/g)) { + const m = /^\s*["']([^"']+)["']/.exec(call.argsText); + if (m) events.push(m[1]); + } + + // 名称与描述:目录输入(包)时用目录名作 base,单文件输入用文件名 + const base = opts.dirName || path.basename(src, ext); + const presetId = slugify(pkgMeta ? (pkgMeta.name || base) : base) || 'from-mcode'; + const presetName = pkgMeta ? pkgMeta.name : base; + const presetDesc = pkgMeta && pkgMeta.description ? pkgMeta.description : `由 mcode/pi extension「${base}」转换生成的 DSH agent preset(桥接模式)`; + + // ── vendor 文件准备 ── + const vendorDir = path.join(outDir, 'vendor'); + fs.mkdirSync(vendorDir, { recursive: true }); + let vendorFileName = path.basename(src); + let vendorSource = source; + if (isTs) { + // 生成剥离类型后的 .js + const stripped = stripTypes(source); + vendorFileName = path.basename(src, '.ts') + '.js'; + fs.writeFileSync(path.join(vendorDir, vendorFileName), stripped, 'utf8'); + fs.copyFileSync(src, path.join(vendorDir, path.basename(src))); // 保留原 .ts + const chk = checkSyntax(path.join(vendorDir, vendorFileName)); + if (!chk.ok) { + warnings.push('TS 类型剥离后语法校验失败:' + chk.error.slice(0, 300) + ' —— 请手工把 extension 编译为 JS 后再转换'); + } + warnings.push('TS 源文件已做保守类型剥离(vendor/' + vendorFileName + '),请人工核对'); + } else { + fs.writeFileSync(path.join(vendorDir, vendorFileName), vendorSource, 'utf8'); + } + // 重写:typebox → 本地垫片;pi 运行时导入 → 注释 + const rewritten = rewriteVendorSource(vendorSource, vendorFileName, warnings); + fs.writeFileSync(path.join(vendorDir, vendorFileName), rewritten, 'utf8'); + if (rewritten.includes('from "./typebox-shim.js"')) { + fs.writeFileSync(path.join(vendorDir, 'typebox-shim.js'), typeboxShimCode(), 'utf8'); + } + // 复制源包同目录的辅助文件,让带相对导入的 extension 包(如 crossplug dsh2mcode 产物: + // vendor/dsh-plugin.js + typebox-shim.js)转换后自包含。 + // 不覆盖已生成的 vendorFileName(源 vendor/ 里若有同名文件以转换产物为准)。 + const srcVendorDir = path.join(srcDir, 'vendor'); + if (fs.existsSync(srcVendorDir)) { + fs.mkdirSync(vendorDir, { recursive: true }); + for (const entry of fs.readdirSync(srcVendorDir, { withFileTypes: true })) { + if (!entry.isDirectory() && entry.name === vendorFileName) continue; + const s = path.join(srcVendorDir, entry.name); + const d = path.join(vendorDir, entry.name); + if (entry.isDirectory()) copyDir(s, d); + else if (entry.isFile()) fs.copyFileSync(s, d); + } + warnings.push('源包 vendor/ 依赖已复制到输出 vendor/(' + fs.readdirSync(srcVendorDir).filter((n) => n !== vendorFileName).join('、') + ')'); + } + for (const aux of ['typebox-shim.js', 'typebox-shim.mjs']) { + const p = path.join(srcDir, aux); + if (fs.existsSync(p) && !fs.existsSync(path.join(vendorDir, aux))) { + fs.copyFileSync(p, path.join(vendorDir, aux)); + warnings.push('源包辅助文件 ' + aux + ' 已复制到输出 vendor/'); + } + } + // 包内 skills 目录 + const srcSkills = path.join(srcDir, 'skills'); + const hasSkills = fs.existsSync(srcSkills); + + // ── 生成 agent.cordis.yml(仅 preset 模式)── + if (!isHost) { + const personaText = [ + `你是由 mcode/pi extension「${presetName}」转换而来的 DSH agent。`, + `转换模式:运行时桥接 —— 原 extension 的工厂函数在本会话进程中执行,`, + `其 registerTool / registerCommand 注册的能力已映射为 DSH 工具与命令。`, + presetDesc, + ].join('\n'); + const compositionRows = [ + { id: 'persona', name: '@deepseek-ai/dsh-persona', configText: personaText }, + { id: 'converted-plugin', name: './plugins/bridge.js' }, + ]; + const { renderPresetYaml, renderCompositionYaml } = require('./yaml.js'); + fs.writeFileSync(path.join(outDir, 'preset.yml'), renderPresetYaml({ name: presetName, description: presetDesc, order: 90 }), 'utf8'); + fs.writeFileSync( + path.join(outDir, 'agent.cordis.yml'), + renderCompositionYaml(compositionRows, [ + '由 crossplug (core/mcode2dsh.js) 生成 —— 桥接自 pi extension ' + path.basename(src), + '桥接插件(plugins/bridge.js)在本 preset 目录内自包含,零 npm 依赖。', + '原 extension 与剥离/重写后的副本在 vendor/。', + ]), + 'utf8', + ); + } + // ── 生成 lib/index.js(宿主组合插件形态:所有会话可用,不依赖 preset 选择)── + const bridge = renderBridge(presetId, vendorFileName, tools, commands, events, warnings, isHost); + if (isHost) { + fs.mkdirSync(path.join(outDir, 'lib'), { recursive: true }); + fs.writeFileSync(path.join(outDir, 'lib', 'index.js'), bridge, 'utf8'); + fs.writeFileSync(path.join(outDir, 'package.json'), JSON.stringify({ + name: presetId, + version: '0.1.0', + description: `由 mcode/pi extension「${presetName}」转换生成的 DSH 宿主组合插件(crossplug 桥接)`, + type: 'module', + main: 'lib/index.js', + }, null, 2) + '\n', 'utf8'); + fs.writeFileSync(path.join(outDir, 'README.md'), [ + '# ' + presetId, + '', + '由 crossplug (core/mcode2dsh.js) 从 mcode/pi extension「' + presetName + '」转换生成的宿主组合插件。', + '所有会话可用,不依赖 agent preset。安装:', + ' 1. 复制本目录到 ~/.dsh/profiles//plugins/' + presetId + '/', + ' 2. 在 profile 的 cordis.patch.yml 追加:', + ' - insert:', + ' - id: ' + presetId, + " name: './plugins/" + presetId + "/lib/index.js'", + ' 3. 重启 DSH 进程。', + '', + '或直接运行:node core/run.js install --side dsh-host --src <本目录>', + '', + ].join('\n') + '\n', 'utf8'); + } else { + fs.mkdirSync(path.join(outDir, 'plugins'), { recursive: true }); + fs.writeFileSync(path.join(outDir, 'plugins', 'bridge.js'), bridge, 'utf8'); + } + // ── 报告 ── + const report = [ + `# 转换报告:mcode/pi extension → DSH ${isHost ? '宿主组合插件(host plugin)' : 'agent preset'}`, + '', + `- 来源: ${src}`, + `- 插件 id(建议目录名): ${presetId}`, + `- 生成时间: ${new Date().toISOString()}`, + '', + `## 识别到的注册`, + '', + `- 工具(${tools.length}): ${tools.map((t) => '`' + t + '`').join('、') || '(未识别到)'}`, + `- 命令(${commands.length}): ${commands.map((c) => '/' + c).join('、') || '(未识别到)'}`, + `- 事件(${events.length}): ${events.map((e) => '`' + e + '`').join('、') || '(未识别到)'}`, + '', + `## 警告`, + '', + ...(warnings.length ? warnings.map((w) => `- ${w}`) : ['(无)']), + '', + `## 说明`, + '', + `- 运行时桥接器会把 pi 的 registerTool 映射为 DSH 工具注册(参数 schema 自动转换,输出为宽松 JSON)。`, + `- registerCommand 映射为 DSH 斜杠命令(handler 包装为 CommandResult)。`, + `- 事件桥接:before_agent_start → system-prompt/assemble(1:1);turn_end / turn_start / tool_call / tool_result / message_start / message_end → session/event 子事件(弱/只读转发;pi 的 block/结果改写能力不生效,详见 doc/event-mapping-questions.md)。`, + `- 工具 execute 中引用的 pi 运行时 API(如 ctx.ui)由垫片提供最小实现,复杂交互需人工适配。`, + '', + ].join('\n') + '\n'; + fs.writeFileSync(path.join(outDir, 'CONVERSION-REPORT.md'), report, 'utf8'); + + return { + kind: isHost ? 'mcode-extension-host' : 'mcode-extension', + source: src, + out: outDir, + presetId, + presetName, + tools: tools.length, + commands: commands.length, + events: events.length, + warnings: warnings.length, + reportFile: path.join(outDir, 'CONVERSION-REPORT.md'), + }; +} + +function renderBridge(presetId, vendorFileName, tools, commands, events, warnings, isHost) { + const toolList = JSON.stringify(tools, null, 2); + const commandList = JSON.stringify(commands, null, 2); + const eventList = JSON.stringify(events, null, 2); + return `// 由 crossplug (core/mcode2dsh.js) 生成的运行时桥接插件。 +// 零 npm 依赖:只用 DSH 的 ctx 服务(tools / commands)与 node 内置能力。 +// 它加载 vendor/${vendorFileName}(原 pi extension),用 pi-API 垫片调用其默认导出工厂, +// 把 registerTool / registerCommand 映射为 DSH 注册。 + +${INLINE_BRIDGE_HELPERS} +function piShim(ctx, log) { + const tools = ctx.get('tools'); + const commands = ctx.get('commands'); + const registered = []; + const warned = new Set(); + const warnOnce = (what) => { + if (warned.has(what)) return; + warned.add(what); + log('[bridge] ' + what); + }; + const shimCtx = { + ui: { + notify: (msg) => log('[bridge:ui.notify] ' + String(msg)), + setStatus: (key, text) => log('[bridge:ui.status] ' + key + ' = ' + text), + setWidget: (key, lines) => log('[bridge:ui.widget] ' + key), + confirm: async () => true, + input: async (question) => { log('[bridge:ui.input] ' + question); return ''; }, + select: async () => null, + custom: () => { warnOnce('ctx.ui.custom 无 DSH 对应,已空操作'); return { render() {} }; }, + }, + session: {}, + // 逆向事件桥接的 pi handler 收集桶(on() 对可桥接事件写入) + _piListeners: {}, + }; + + const api = { + registerTool(t) { + if (!t || typeof t.name !== 'string') { warnOnce('registerTool 缺少 name,跳过'); return; } + if (!tools) { warnOnce('DSH tools 服务不可用,工具 ' + t.name + ' 未注册'); return; } + try { + const schema = tbToJsonSchema(t.parameters); + tools.register({ + name: t.name, + description: t.description || '', + parameters: schema, + output: { + schema: {}, + render(_args, value) { + let text = ''; + if (value && typeof value === 'object' && Array.isArray(value.content)) { + text = value.content.map((c) => (c && typeof c.text === 'string' ? c.text : JSON.stringify(c))).join('\\n'); + } else { + text = JSON.stringify(value, null, 2); + } + return [{ type: 'text', text }]; + }, + }, + async execute(args, exec) { + if (typeof t.execute !== 'function') { + return { content: [{ type: 'text', text: '[桥接桩] ' + t.name + ' 未提供 execute' }], details: {} }; + } + try { + const r = await t.execute(exec.callId, args, exec.signal, undefined, shimCtx); + if (r && typeof r === 'object' && Array.isArray(r.content)) return r; + return r; + } catch (e) { + throw new Error('[bridge:' + t.name + '] ' + (e && e.message ? e.message : String(e))); + } + }, + }); + registered.push('tool:' + t.name); + } catch (e) { + log('[bridge] 工具 ' + t.name + ' 注册失败: ' + e.message); + } + }, + registerCommand(name, def) { + if (!commands || !name) { warnOnce('commands 服务不可用或命令缺名: ' + name); return; } + try { + commands.register({ + name, + description: (def && def.description) || '', + async handler(invocation) { + try { + const r = await def.handler((invocation.rawInput || '').trim(), shimCtx); + const text = typeof r === 'string' ? r : r === undefined || r === null ? '' : JSON.stringify(r, null, 2); + return { kind: 'success', text }; + } catch (e) { + return { kind: 'error', text: '[bridge:' + name + '] ' + (e && e.message ? e.message : String(e)) }; + } + }, + }); + registered.push('command:' + name); + } catch (e) { + log('[bridge] 命令 /' + name + ' 注册失败: ' + e.message); + } + }, + registerShortcut: (s) => warnOnce('registerShortcut 无 DSH 对应,已跳过: ' + JSON.stringify(s)), + registerFlag: (f) => warnOnce('registerFlag 无 DSH 对应,已跳过: ' + JSON.stringify(f)), + registerProvider: (p) => warnOnce('registerProvider 无 DSH 对应,已跳过: ' + JSON.stringify(p)), + appendEntry: () => warnOnce('appendEntry 无 DSH 对应,已跳过'), + on(event, handler) { + // 可逆向桥接的 pi 事件(对照 mcode-plugin-spec.md §5.2 / §15.3): + // before_agent_start → system-prompt/assemble(1:1) + // turn_end → session/event(turn/end)(1:1) + // turn_start → session/event(turn/start)(弱) + // tool_call → session/event(tool/call)(只读转发;pi 的 block 能力 DSH 侧不存在) + // tool_result → session/event(tool/result)(只读转发;pi 的结果改写能力 DSH 侧不存在) + // message_start → session/event(user/message)(弱) + // message_end → session/event(assistant/message)(弱) + // 其余事件:跳过并记日志(逻辑保留在 vendor 文件中)。 + const BRIDGE_EVENTS = { + before_agent_start: 'system-prompt/assemble', + turn_end: 'session/event', + turn_start: 'session/event', + tool_call: 'session/event', + tool_result: 'session/event', + message_start: 'session/event', + message_end: 'session/event', + }; + if (BRIDGE_EVENTS[event]) { + const list = shimCtx._piListeners[event] || (shimCtx._piListeners[event] = []); + list.push(handler); + return; + } + warnOnce('pi 事件 ' + event + ' 无 1:1 DSH 事件,监听已跳过(逻辑保留在 vendor 文件中)'); + }, + }; + + return { api, registered, shimCtx }; +} + +export default { + // host 模式(宿主组合插件)声明式依赖;preset 模式由组合行内联激活 + inject: ['tools', 'commands'], + async apply(ctx) { + const log = (msg) => { try { console.log('[crossplug:' + ${JSON.stringify(presetId)} + ']', msg); } catch { /* 忽略 */ } }; + try { + const mod = await import('../vendor/${vendorFileName}'); + const factory = mod && (mod.default !== undefined ? mod.default : mod); + if (typeof factory !== 'function') { + log('vendor/${vendorFileName} 没有默认导出工厂,无法桥接'); + return; + } + const { api, registered, shimCtx } = piShim(ctx, log); + await factory(api); + log('桥接完成,注册了: ' + (registered.length ? registered.join(', ') : '(无)')); + + // ── 逆向事件桥接:pi 事件 → DSH 事件 ── + // 1:1:before_agent_start → system-prompt/assemble(DSH host 平面每会话首轮组装, + // handler(assembly, context, next),assembly.sections.push({ name, text }) 注入段落)。 + // 弱/只读:turn_end / turn_start / tool_call / tool_result / message_start / message_end + // → session/event(DSH handler(session, event) 按 event.type 过滤,payload 在 event.data; + // KNOWN_SESSION_EVENT_TYPES:turn/end、turn/start、tool/call、tool/result、 + // user/message、assistant/message)。pi 的 block(tool_call)与结果改写(tool_result) + // 能力在 DSH 侧不存在——只读转发,返回值忽略。 + // 仅当 ctx 提供 on()(宿主组合/测试 mock 可能没有)且收集到 handler 时注册。 + const piListeners = shimCtx._piListeners || {}; + if (ctx && typeof ctx.on === 'function') { + if (piListeners['before_agent_start'] && piListeners['before_agent_start'].length) { + try { + ctx.on('system-prompt/assemble', async (assembly, _context, next) => { + for (const handler of piListeners['before_agent_start']) { + try { + const r = await handler({ systemPrompt: '' }, shimCtx); + if (r && typeof r.systemPrompt === 'string' && r.systemPrompt.length) { + assembly.sections.push({ name: 'mcode-bridge:' + ${JSON.stringify(presetId)}, text: r.systemPrompt }); + } + } catch (e) { + log('before_agent_start handler 抛错: ' + (e && e.message ? e.message : String(e))); + } + } + // DSH 的 system-prompt/assemble 是链式监听(原 dsh-hindsight-client 实证): + // 必须调用 next() 继续链,否则宿主组合的组装链中断 + return next(); + }); + log('事件桥接: before_agent_start → system-prompt/assemble'); + } catch (e) { + log('system-prompt/assemble 桥接注册失败: ' + (e && e.message ? e.message : String(e))); + } + } + const hasSessionEvent = ['turn_end', 'turn_start', 'tool_call', 'tool_result', 'message_start', 'message_end'] + .some((k) => piListeners[k] && piListeners[k].length); + if (hasSessionEvent) { + try { + ctx.on('session/event', async (session, event) => { + const type = event && event.type; + const data = event && event.data !== undefined ? event.data : event; + const fire = async (key, ev) => { + for (const handler of (piListeners[key] || [])) { + try { await handler(ev, shimCtx); } + catch (e) { log(key + ' handler 抛错: ' + (e && e.message ? e.message : String(e))); } + } + }; + if (type === 'turn/end') { + // Q2:pi turn_end.message 是 AgentMessage;DSH 侧 session.deriveMessages() 提供消息数组, + // 尽力桥接(vendor 的 auto-retain 等依赖消息文本的逻辑可工作) + let msgs = []; + try { + const dm = session && typeof session.deriveMessages === 'function' ? session.deriveMessages() : null; + if (Array.isArray(dm)) msgs = dm; + } catch { /* 忽略 */ } + await fire('turn_end', { turnIndex: data.turn !== undefined ? data.turn : 0, toolResults: [], message: msgs.length ? msgs : undefined }); + } else if (type === 'turn/start') { + await fire('turn_start', { turnIndex: data.turn !== undefined ? data.turn : 0 }); + } else if (type === 'tool/call') { + // pi ToolCallEvent:{ type, toolCallId, toolName, input }(Q3 已确认) + // DSH arguments 是模型原始 JSON 字符串 → 解析后赋给 input + let input = data.arguments; + if (typeof input === 'string') { try { input = JSON.parse(input); } catch { /* 保留原文 */ } } + await fire('tool_call', { type: 'tool_call', toolCallId: data.callId, toolName: data.name, input }); + } else if (type === 'tool/result') { + // pi ToolResultEvent:{ type, toolCallId, toolName, input, content, details?, isError, usage? }(Q4 已确认) + // DSH tool/result payload 无 callId/name({turn, step, message, error?, meta?})—— + // 从 message(ToolResultMessage,含 callId?/name?/content/details/isError)回溯(Q4 补充答复确认方向正确) + const msg = data.message || {}; + await fire('tool_result', { + type: 'tool_result', + toolCallId: msg.callId, + toolName: msg.name, + input: {}, + content: msg.content, + details: msg.details, + isError: msg.isError, + usage: msg.usage, + }); + } else if (type === 'user/message') { + // pi MessageStartEvent:{ type, message: AgentMessage }(Q5 已确认); + // DSH user/message 本身即用户消息,天然对应 pi 的 user 角色 + await fire('message_start', { type: 'message_start', message: data }); + } else if (type === 'assistant/message') { + await fire('message_end', { type: 'message_end', message: data.message }); + } + }); + log('事件桥接: session/event ← turn_end/turn_start/tool_call/tool_result/message_start/message_end'); + } catch (e) { + log('session/event 桥接注册失败: ' + (e && e.message ? e.message : String(e))); + } + } + } + } catch (e) { + log('桥接失败: ' + (e && e.message ? e.stack || e.message : String(e))); + } + }, +}; + +// ── 生成期提取的注册清单(仅报告参考)── +// tools: ${toolList.replace(/\n/g, '\n// ')} +// commands: ${commandList.replace(/\n/g, '\n// ')} +// events: ${eventList.replace(/\n/g, '\n// ')} +`; +} + +function slugify(id) { + return String(id || 'plugin') + .toLowerCase() + .replace(/^@/, '') + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + .slice(0, 48); +} + +// ═══ 模式二:MCode 插件包(agent-plugins / MiniMax / Claude 兼容 manifest)→ DSH preset ═══ + +const PLUGIN_MANIFESTS = [ + { file: 'plugin.json', kind: 'AGENT_PLUGINS_V1' }, + { file: path.join('.minimax-plugin', 'plugin.json'), kind: 'MINIMAX' }, + { file: path.join('.claude-plugin', 'plugin.json'), kind: 'CLAUDE_CODE' }, +]; + +function hasPluginManifest(dir) { + return PLUGIN_MANIFESTS.some((m) => fs.existsSync(path.join(dir, m.file))); +} + +function convertPluginPackage(srcDir, outDir) { + const entry = PLUGIN_MANIFESTS.find((m) => fs.existsSync(path.join(srcDir, m.file))); + if (!entry) throw new Error('目录中没有插件 manifest(plugin.json / .minimax-plugin/plugin.json / .claude-plugin/plugin.json)'); + const manifestPath = path.join(srcDir, entry.file); + let manifest; + try { + manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + } catch (e) { + throw new Error(`插件 manifest 解析失败: ${manifestPath}(${e.message})`); + } + const name = manifest.name || path.basename(srcDir); + const description = manifest.description || `由 mcode 插件「${name}」转换生成的 DSH agent preset`; + const presetId = slugify(name) || 'from-mcode-plugin'; + const warnings = []; + + // skills/ 目录复制(名字规范化) + const srcSkills = path.join(srcDir, 'skills'); + const hasSkills = fs.existsSync(srcSkills); + if (hasSkills) { + fs.mkdirSync(path.join(outDir, 'skills'), { recursive: true }); + for (const child of fs.readdirSync(srcSkills, { withFileTypes: true })) { + if (!child.isDirectory()) continue; + const safe = slugify(child.name) || 'skill'; + copyDir(path.join(srcSkills, child.name), path.join(outDir, 'skills', safe)); + if (safe !== child.name) warnings.push(`skill 目录「${child.name}」重命名为 ${safe}`); + } + } + + // mcp.json 读取(仅报告,DSH 侧 MCP 需另行配置) + const mcpPath = path.join(srcDir, 'mcp.json'); + const mcpNames = []; + if (fs.existsSync(mcpPath)) { + try { + const mcp = JSON.parse(fs.readFileSync(mcpPath, 'utf8')); + mcpNames.push(...Object.keys(mcp.mcpServers || {})); + } catch { /* 忽略坏 mcp.json */ } + } + const claudeMcp = path.join(srcDir, '.mcp.json'); + if (fs.existsSync(claudeMcp)) { + try { + const mcp = JSON.parse(fs.readFileSync(claudeMcp, 'utf8')); + mcpNames.push(...Object.keys(mcp.mcpServers || {})); + } catch { /* 忽略 */ } + } + + // 生成 preset + const personaText = [ + `你是由 mcode 插件「${name}」转换而来的 DSH agent(manifest: ${entry.kind})。`, + `插件携带的 skills 已挂载到本 preset(skills/ 目录)。`, + description, + ].join('\n'); + + const compositionRows = [ + { id: 'persona', name: '@deepseek-ai/dsh-persona', configText: personaText }, + ]; + if (hasSkills) { + compositionRows.push({ + id: 'skill-filesystem', + name: '@deepseek-ai/dsh-skill-filesystem', + configScalarList: { + key: 'customSkillDirs', + items: ['!!js "process.getBuiltinModule(\'node:url\').fileURLToPath(new URL(\'skills/\', baseUrl))"'], + }, + }); + } + const { renderPresetYaml, renderCompositionYaml } = require('./yaml.js'); + + fs.mkdirSync(outDir, { recursive: true }); + fs.writeFileSync(path.join(outDir, 'preset.yml'), renderPresetYaml({ name, description, order: 90 }), 'utf8'); + fs.writeFileSync( + path.join(outDir, 'agent.cordis.yml'), + renderCompositionYaml(compositionRows, [ + `由 crossplug (core/mcode2dsh.js) 生成 —— 转换自 mcode 插件包 ${path.basename(srcDir)}(manifest: ${entry.kind})`, + '插件 skills 已复制到本 preset 的 skills/,由 dsh-skill-filesystem 挂载。', + '插件中的 MCP 服务器不随 preset 迁移,需在 DSH 侧另行配置(见 CONVERSION-REPORT.md)。', + ]), + 'utf8', + ); + + // 报告 + const report = [ + `# 转换报告:mcode 插件包 → DSH agent preset`, + '', + `- 来源: ${srcDir}`, + `- manifest: ${manifestPath}(${entry.kind})`, + `- 插件名: ${name}`, + `- preset id(建议目录名): ${presetId}`, + `- 生成时间: ${new Date().toISOString()}`, + '', + `## 能力映射`, + '', + `- skills(${hasSkills ? '已复制到 preset skills/' : '无'})`, + `- MCP 服务器(${mcpNames.length ? mcpNames.join('、') : '无'}):DSH 侧不支持从插件包自动迁移 MCP,需在 DSH 配置中手工添加。`, + `- apps / extensions 字段:不在转换范围内。`, + '', + `## 警告`, + '', + ...(warnings.length ? warnings.map((w) => `- ${w}`) : ['(无)']), + '', + ].join('\n') + '\n'; + fs.writeFileSync(path.join(outDir, 'CONVERSION-REPORT.md'), report, 'utf8'); + + return { + kind: 'mcode-plugin-package', + source: srcDir, + out: outDir, + presetId, + presetName: name, + manifestKind: entry.kind, + skills: hasSkills, + mcpServers: mcpNames, + warnings: warnings.length, + reportFile: path.join(outDir, 'CONVERSION-REPORT.md'), + }; +} + +function copyDir(src, dst) { + fs.mkdirSync(dst, { recursive: true }); + for (const child of fs.readdirSync(src, { withFileTypes: true })) { + const s = path.join(src, child.name); + const d = path.join(dst, child.name); + if (child.isDirectory()) copyDir(s, d); + else if (child.isFile()) fs.copyFileSync(s, d); + } +} + +// ═══ 入口 ═══ + +function convertInput(src, outDir, opts = {}) { + const stat = fs.statSync(src); + if (stat.isDirectory()) { + if (hasPluginManifest(src)) return convertPluginPackage(src, outDir); + const found = locateExtension(src); + if (!found) throw new Error(`目录中没有找到 extension 文件(index.js/extension.js/main.js/index.ts 等): ${src}`); + return convert(found.file, outDir, { pkg: found.pkg, dirName: path.basename(src), host: !!opts.host }); + } + if (stat.isFile()) return convert(src, outDir, { host: !!opts.host }); + throw new Error(`无法识别的输入: ${src}`); +} + +module.exports = { convert: convertInput, locateExtension, stripTypes, convertPluginPackage, hasPluginManifest }; diff --git a/plugins/Fectivnfy112357/crossplug/core/package.json b/plugins/Fectivnfy112357/crossplug/core/package.json new file mode 100644 index 0000000..67e69b8 --- /dev/null +++ b/plugins/Fectivnfy112357/crossplug/core/package.json @@ -0,0 +1,5 @@ +{ + "name": "crossplug-core", + "private": true, + "type": "commonjs" +} diff --git a/plugins/Fectivnfy112357/crossplug/core/run.js b/plugins/Fectivnfy112357/crossplug/core/run.js new file mode 100644 index 0000000..1e402da --- /dev/null +++ b/plugins/Fectivnfy112357/crossplug/core/run.js @@ -0,0 +1,242 @@ +#!/usr/bin/env node +// run.js — crossplug 转换引擎入口(零依赖) +// +// 用法: +// node core/run.js dsh2mcode [--out ] [--json] +// node core/run.js mcode2dsh [--out ] [--json] +// node core/run.js list --side [--json] +// node core/run.js install --side [--force] [--preset-id ] [--ext-dir ] [--profile ] [--json] +// +// 输出:人类可读文本或 --json 的 JSON 对象(供适配器解析)。 + +'use strict'; + +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const dsh2mcode = require('./dsh2mcode.js'); +const mcode2dsh = require('./mcode2dsh.js'); +const { installDsh, installDshHost, installDshHostPlugin, installMcode, registerPluginSkills, dshHome, minimaxHome, piHome } = require('./install.js'); + +function fail(message) { + process.stderr.write('错误: ' + message + '\n'); + process.exit(1); +} + +function argsOf(argv) { + const out = { positional: [], flags: {} }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a.startsWith('--')) { + const eq = a.indexOf('='); + if (eq > 0) { + out.flags[a.slice(2, eq)] = a.slice(eq + 1); + } else { + const next = argv[i + 1]; + if (next !== undefined && !next.startsWith('--')) { + out.flags[a.slice(2)] = next; + i++; + } else { + out.flags[a.slice(2)] = true; + } + } + } else { + out.positional.push(a); + } + } + return out; +} + +function defaultOutDir(src, sub) { + const base = path.basename(path.resolve(src)); + return path.join(process.cwd(), 'out', sub + '-' + base); +} + +function listDsh() { + const userDir = path.join(dshHome(), '.agent-presets'); + const result = []; + if (fs.existsSync(userDir)) { + for (const d of fs.readdirSync(userDir, { withFileTypes: true })) { + if (d.isDirectory() && fs.existsSync(path.join(userDir, d.name, 'agent.cordis.yml'))) { + result.push({ id: d.name, scope: 'user', dir: path.join(userDir, d.name) }); + } + } + } + // 部署自带 preset(通过 npm 全局根定位 @deepseek-ai/dsh 安装目录) + for (const root of globalNpmRoots()) { + const deployDir = path.join(root, '@deepseek-ai', 'dsh', 'config', 'agent-presets'); + if (fs.existsSync(deployDir)) { + for (const d of fs.readdirSync(deployDir, { withFileTypes: true })) { + if (d.isDirectory() && fs.existsSync(path.join(deployDir, d.name, 'agent.cordis.yml'))) { + result.push({ id: d.name, scope: 'deployment', dir: path.join(deployDir, d.name) }); + } + } + break; + } + } + return result; +} + +function globalNpmRoots() { + const roots = []; + try { + const { spawnSync } = require('node:child_process'); + // Windows 下 npm.cmd 需要 shell;先用 cmd /c 试,失败静默 + const cmd = process.platform === 'win32' + ? spawnSync('cmd', ['/c', 'npm root -g'], { encoding: 'utf8', windowsHide: true }) + : spawnSync('npm', ['root', '-g'], { encoding: 'utf8', windowsHide: true }); + if (cmd.status === 0 && cmd.stdout && cmd.stdout.trim()) roots.push(cmd.stdout.trim()); + } catch { /* 忽略 */ } + // 从 node 可执行文件位置推导:/node_global/node_modules(Windows npm 常见布局) + try { + const exeDir = path.dirname(process.execPath); + roots.push(path.join(exeDir, 'node_global', 'node_modules')); + roots.push(path.join(path.dirname(exeDir), 'lib', 'node_modules')); + roots.push(path.join(path.dirname(exeDir), 'lib')); + } catch { /* 忽略 */ } + // 兜底常见位置 + if (process.env.APPDATA) roots.push(path.join(process.env.APPDATA, 'npm', 'node_modules')); + const home = os.homedir(); + roots.push(path.join(home, '.local', 'lib', 'node_modules')); + roots.push(path.join(home, '.npm', 'global', 'node_modules')); + return roots; +} + +function listMcode() { + const result = []; + const pluginsDir = path.join(minimaxHome(), 'plugins'); + if (fs.existsSync(pluginsDir)) { + for (const d of fs.readdirSync(pluginsDir, { withFileTypes: true })) { + if (d.isDirectory()) result.push({ id: d.name, scope: 'minimax-plugins', dir: path.join(pluginsDir, d.name) }); + } + } + const extDir = path.join(piHome(), 'agent', 'extensions'); + if (fs.existsSync(extDir)) { + for (const d of fs.readdirSync(extDir, { withFileTypes: true })) { + const p = path.join(extDir, d.name); + if (d.isDirectory()) result.push({ id: d.name, scope: 'pi-extensions', dir: p }); + else if (d.isFile() && /\.(js|ts)$/.test(d.name)) result.push({ id: d.name, scope: 'pi-extensions', dir: p }); + } + } + return result; +} + +function main() { + const argv = process.argv.slice(2); + const cmd = argv[0]; + if (!cmd) { + process.stdout.write(usage()); + return; + } + if (cmd === 'version' || cmd === '--version' || cmd === '-v') { + process.stdout.write('crossplug 0.1.0\n'); + return; + } + const rest = argv.slice(1); + const a = argsOf(rest); + const json = !!a.flags.json; + + try { + let result; + if (cmd === 'dsh2mcode') { + const src = a.positional[0]; + if (!src) fail('dsh2mcode 需要输入路径(preset 目录或插件源码文件)'); + const out = a.flags.out || defaultOutDir(src, 'mcode'); + result = dsh2mcode.convert(path.resolve(src), path.resolve(out)); + result.ok = true; + // 输出落在 ~/.minimax/plugins/ 下时:把包内技能自动注册为用户技能(斜杠命令可用) + const outAbs = path.resolve(out); + const mmPlugins = path.join(minimaxHome(), 'plugins'); + if (outAbs === mmPlugins || outAbs.startsWith(mmPlugins + path.sep)) { + result.skillCommands = registerPluginSkills(outAbs); + } + } else if (cmd === 'mcode2dsh') { + const src = a.positional[0]; + if (!src) fail('mcode2dsh 需要输入路径(extension 文件或插件包目录)'); + const out = a.flags.out || defaultOutDir(src, 'preset'); + result = mcode2dsh.convert(path.resolve(src), path.resolve(out), { host: !!a.flags.host }); + result.ok = true; + } else if (cmd === 'list') { + const side = a.flags.side; + if (side === 'dsh') result = { ok: true, side, plugins: listDsh() }; + else if (side === 'mcode') result = { ok: true, side, plugins: listMcode() }; + else fail('list 需要 --side dsh|mcode'); + } else if (cmd === 'install') { + const side = a.flags.side; + if (side === 'dsh') result = installDsh({ presetId: a.flags['preset-id'] || 'crossplug', force: !!a.flags.force }); + else if (side === 'mcode') result = installMcode({ + extDir: a.flags['ext-dir'] ? path.resolve(a.flags['ext-dir']) : undefined, + srcDir: a.flags.src ? path.resolve(a.flags.src) : undefined, + force: !!a.flags.force, + piSettings: !!a.flags['pi-settings'], + }); + else if (side === 'dsh-host') { + if (a.flags.src) result = installDshHostPlugin(path.resolve(a.flags.src), { profile: a.flags.profile, force: !!a.flags.force }); + else result = installDshHost({ profile: a.flags.profile, force: !!a.flags.force }); + } + else fail('install 需要 --side dsh|mcode|dsh-host'); + } else { + fail('未知命令: ' + cmd + '\n\n' + usage()); + } + + if (json) { + process.stdout.write(JSON.stringify(result, null, 2) + '\n'); + } else { + process.stdout.write(render(result) + '\n'); + } + } catch (e) { + if (json) { + process.stdout.write(JSON.stringify({ ok: false, error: e.message }) + '\n'); + process.exit(1); + } + fail(e.message); + } +} + +function render(r) { + const lines = []; + if (r.ok === false) return '错误: ' + r.error; + if (r.plugins !== undefined) { + lines.push(`【${r.side === 'dsh' ? 'DSH' : 'mcode'}】插件清单(${r.plugins.length}):`); + for (const p of r.plugins) lines.push(` - ${p.id} (${p.scope}) ${p.dir}`); + return lines.join('\n'); + } + if (r.side !== undefined) { + const label = r.side === 'dsh' ? 'DSH' : r.side === 'dsh-host' ? 'DSH(宿主组合)' : 'mcode'; + lines.push(`✔ 已安装到 ${label}:`); + for (const f of r.files) lines.push(` - ${f}`); + lines.push(` 目录: ${r.target || r.extDir}`); + if (r.settingsFile) lines.push(` settings: ${r.settingsFile}`); + if (r.patchFile) lines.push(` patch: ${r.patchFile}(${r.patchAction})`); + lines.push(` 下一步: ${r.next}`); + return lines.join('\n'); + } + lines.push(`✔ 转换完成: ${r.kind}`); + lines.push(` 来源: ${r.source}`); + lines.push(` 输出: ${r.out}`); + if (r.presetName) lines.push(` 名称: ${r.presetName}`); + if (r.packageName) lines.push(` 包名: ${r.packageName}`); + if (r.rows !== undefined) lines.push(` preset 行: ${r.rows},生成桩工具 ${r.tools} / 命令 ${r.commands},说明项 ${r.notes}`); + if (r.tools !== undefined && r.kind === 'mcode-extension') { + lines.push(` 工具: ${r.tools},命令: ${r.commands},事件: ${r.events},警告: ${r.warnings}`); + } + if (r.warnings !== undefined && r.kind !== 'mcode-extension') lines.push(` 警告: ${r.warnings}`); + if (r.reportFile) lines.push(` 报告: ${r.reportFile}`); + return lines.join('\n'); +} + +function usage() { + return `crossplug — DSH 插件 ↔ mcode(MiniMax Code / pi)插件双向转换 + +用法: + node core/run.js dsh2mcode [--out ] + node core/run.js mcode2dsh [--out ] [--host] + node core/run.js list --side dsh|mcode + node core/run.js install --side dsh|mcode|dsh-host [--force] [--preset-id ] [--ext-dir ] [--profile ] [--src ] + node core/run.js version + +所有命令支持 --json 输出(供插件/脚本调用)。 +`; +} + +main(); diff --git a/plugins/Fectivnfy112357/crossplug/core/schema.js b/plugins/Fectivnfy112357/crossplug/core/schema.js new file mode 100644 index 0000000..06eaa4a --- /dev/null +++ b/plugins/Fectivnfy112357/crossplug/core/schema.js @@ -0,0 +1,224 @@ +// schema.js — JSON Schema ↔ typebox 相互转换(零依赖) +// +// dsh2mcode(插件代码模式):JSON Schema → typebox 代码字符串 +// mcode2dsh(桥接模式): typebox 运行时对象 → JSON Schema(鸭子类型识别) + +'use strict'; + +// ── JSON Schema → typebox 代码 ────────────────────────────────────────── + +function jsLiteral(v, indent) { + return JSON.stringify(v, null, indent === undefined ? 0 : indent); +} + +function isObjSchema(node) { + return node && typeof node === 'object' && !Array.isArray(node) && typeof node.type === 'string'; +} + +// 把 JSON Schema 节点渲染成 typebox 表达式代码 +function jsonSchemaToTypeboxCode(node, indent = '') { + if (!isObjSchema(node)) { + if (node && typeof node === 'object' && Array.isArray(node.oneOf)) { + const branches = node.oneOf.map((b) => jsonSchemaToTypeboxCode(b, indent)).join(',\n' + indent + ' '); + return `Type.Union([\n${indent} ${branches}\n${indent}])`; + } + return 'Type.Any()'; + } + const t = node.type; + const opts = []; + if (node.description) opts.push(`description: ${jsLiteral(node.description)}`); + const optText = opts.length ? `{ ${opts.join(', ')} }` : ''; + switch (t) { + case 'object': { + const props = Object.entries(node.properties || {}); + const req = (node.required || []).filter((k) => (node.properties || {})[k]); + if (req.length) opts.push(`required: ${jsLiteral(req)}`); + if (node.additionalProperties === false) opts.push('additionalProperties: false'); + const optText2 = opts.length ? `{ ${opts.join(', ')} }` : ''; + if (!props.length) return `Type.Object({}${optText2 ? ', ' + optText2 : ''})`; + const propsText = props + .map(([k, v]) => ` ${JSON.stringify(k)}: ${jsonSchemaToTypeboxCode(v, indent + ' ')}`) + .join(',\n'); + return `Type.Object({\n${propsText}\n }${optText2 ? ', ' + optText2 : ''})`; + } + case 'array': { + const items = node.items ? jsonSchemaToTypeboxCode(node.items, indent) : 'Type.Any()'; + return `Type.Array(${items}${optText ? ', ' + optText : ''})`; + } + case 'string': { + if (node.enum && node.enum.length) { + // mcode-plugin-spec.md §6.2:字符串枚举必须用 StringEnum(Type.Union/Literal + // 的字符串枚举在 Google API 不工作)。typebox-shim.js 提供 Type.StringEnum。 + const enumText = `[${node.enum.map((e) => jsLiteral(e)).join(', ')}]`; + return optText ? `Type.StringEnum(${enumText}, ${optText})` : `Type.StringEnum(${enumText})`; + } + return `Type.String(${optText})`; + } + case 'number': + return `Type.Number(${optText})`; + case 'integer': + return `Type.Integer(${optText})`; + case 'boolean': + return `Type.Boolean(${optText})`; + case 'null': + return `Type.Null()`; + default: + return 'Type.Any()'; + } +} + +// ── typebox 运行时对象 → JSON Schema ──────────────────────────────────── + +// typebox 的 .type 属性返回大写 Kind('Object'/'String'/...), +// 普通 JSON Schema 的 type 是小写 —— 这是可靠的鸭子类型判据。 +function looksTypebox(schema) { + return ( + schema && + typeof schema === 'object' && + typeof schema.type === 'string' && + /^[A-Z]/.test(schema.type) + ); +} + +function typeboxToJsonSchema(schema) { + if (!looksTypebox(schema)) { + // 已是普通 JSON Schema(或无法识别)——原样返回(只做浅拷贝防御) + if (schema && typeof schema === 'object' && !Array.isArray(schema)) return { ...schema }; + return {}; + } + const kind = schema.type; + const out = {}; + if (schema.description) out.description = schema.description; + switch (kind) { + case 'Object': { + out.type = 'object'; + out.properties = {}; + for (const [k, v] of Object.entries(schema.properties || {})) { + out.properties[k] = typeboxToJsonSchema(v); + } + if (Array.isArray(schema.required) && schema.required.length) out.required = [...schema.required]; + if (schema.additionalProperties !== undefined) out.additionalProperties = !!schema.additionalProperties; + return out; + } + case 'Array': + out.type = 'array'; + if (schema.items) out.items = typeboxToJsonSchema(schema.items); + return out; + case 'Union': + if (Array.isArray(schema.anyOf) && schema.anyOf.length >= 2) { + out.oneOf = schema.anyOf.map((b) => typeboxToJsonSchema(b)); + return out; + } + if (Array.isArray(schema.anyOf) && schema.anyOf.length === 1) return typeboxToJsonSchema(schema.anyOf[0]); + return { type: 'object' }; + case 'Optional': { + // Optional(T) 包裹:类型本身不强制 required + return typeboxToJsonSchema(schema.anyOf && schema.anyOf.length ? schema.anyOf[0] : {}); + } + case 'String': + out.type = 'string'; + if (Array.isArray(schema.anyOf) && schema.anyOf.length) { + const literals = schema.anyOf + .filter((b) => b && b.const !== undefined) + .map((b) => b.const); + if (literals.length) out.enum = literals; + } + return out; + case 'Number': + out.type = 'number'; + return out; + case 'Integer': + out.type = 'integer'; + return out; + case 'Boolean': + out.type = 'boolean'; + return out; + case 'Null': + out.type = 'null'; + return out; + case 'Literal': + if (schema.const !== undefined) { + out.const = schema.const; + out.type = typeof schema.const === 'number' ? (Number.isInteger(schema.const) ? 'integer' : 'number') : typeof schema.const === 'boolean' ? 'boolean' : 'string'; + return out; + } + return {}; + default: + return { type: 'object' }; + } +} + +// typebox 最小垫片源码(生成到输出包内,让转换产物零依赖自包含) +function typeboxShimCode() { + return [ + '// 由 crossplug 生成的 typebox 最小垫片:只实现 schema 构造所需子集,', + '// 运行时对象形态与 typebox 一致(大写 type 字段),供桥接器识别。', + 'function kind(t, props) { const s = { type: t, ...props }; return s; }', + 'export const Type = {', + ' Object: (properties = {}, options = {}) => kind("Object", { properties, required: options.required || [], additionalProperties: options.additionalProperties }),', + ' Array: (items, options = {}) => kind("Array", { items, ...(options.description ? { description: options.description } : {}) }),', + ' String: (options = {}) => kind("String", { ...(options.description ? { description: options.description } : {}) }),', + ' Number: (options = {}) => kind("Number", { ...(options.description ? { description: options.description } : {}) }),', + ' Integer: (options = {}) => kind("Integer", { ...(options.description ? { description: options.description } : {}) }),', + ' Boolean: (options = {}) => kind("Boolean", { ...(options.description ? { description: options.description } : {}) }),', + ' Null: () => kind("Null", {}),', + ' Any: () => kind("Any", {}),', + ' Unknown: () => kind("Any", {}),', + ' Literal: (value) => kind("Literal", { const: value }),', + ' Union: (anyOf) => kind("Union", { anyOf }),', + ' Optional: (inner) => kind("Optional", { anyOf: [inner] }),', + ' StringEnum: (values, options = {}) => kind("String", { anyOf: (values || []).map((v) => ({ const: v })), ...(options && options.description ? { description: options.description } : {}) }),', + ' Record: (key, value) => kind("Object", { properties: {}, additionalProperties: true }),', + '};', + '', + ].join('\n'); +} + +// 供生成文件内联的紧凑版(桥接插件运行时使用) +const INLINE_BRIDGE_HELPERS = ` +function tbToJsonSchema(schema) { + if (!schema || typeof schema !== 'object') return {}; + if (typeof schema.type !== 'string' || !/^[A-Z]/.test(schema.type)) return { ...schema }; + const kind = schema.type; + const out = {}; + if (schema.description) out.description = schema.description; + switch (kind) { + case 'Object': { + out.type = 'object'; + out.properties = {}; + for (const k of Object.keys(schema.properties || {})) out.properties[k] = tbToJsonSchema(schema.properties[k]); + if (Array.isArray(schema.required) && schema.required.length) out.required = schema.required.slice(); + if (schema.additionalProperties !== undefined) out.additionalProperties = !!schema.additionalProperties; + return out; + } + case 'Array': + out.type = 'array'; + if (schema.items) out.items = tbToJsonSchema(schema.items); + return out; + case 'Union': + if (Array.isArray(schema.anyOf) && schema.anyOf.length === 1) return tbToJsonSchema(schema.anyOf[0]); + if (Array.isArray(schema.anyOf) && schema.anyOf.length > 1) { out.oneOf = schema.anyOf.map(tbToJsonSchema); return out; } + return { type: 'object' }; + case 'Optional': + if (Array.isArray(schema.anyOf) && schema.anyOf.length) return tbToJsonSchema(schema.anyOf[0]); + return { type: 'object' }; + case 'String': { + out.type = 'string'; + // StringEnum(values) 在垫片里展开为 { type: 'String', anyOf: [{ const: v }] }: + // 提取 const 列表作为 JSON Schema enum,保持枚举参数语义(mcode-plugin-spec.md §6.2)。 + if (Array.isArray(schema.anyOf) && schema.anyOf.length) { + const literals = schema.anyOf.filter((b) => b && b.const !== undefined).map((b) => b.const); + if (literals.length) out.enum = literals; + } + return out; + } + case 'Number': out.type = 'number'; return out; + case 'Integer': out.type = 'integer'; return out; + case 'Boolean': out.type = 'boolean'; return out; + case 'Null': out.type = 'null'; return out; + default: return { type: 'object' }; + } +} +`; + +module.exports = { jsonSchemaToTypeboxCode, typeboxToJsonSchema, looksTypebox, INLINE_BRIDGE_HELPERS, typeboxShimCode }; diff --git a/plugins/Fectivnfy112357/crossplug/core/yaml.js b/plugins/Fectivnfy112357/crossplug/core/yaml.js new file mode 100644 index 0000000..d851c64 --- /dev/null +++ b/plugins/Fectivnfy112357/crossplug/core/yaml.js @@ -0,0 +1,260 @@ +// yaml.js — DSH agent-preset YAML 子集解析与生成(零依赖) +// +// 只覆盖 DSH preset 实际使用的子集: +// - 顶层标量(name / description / order) +// - 0 缩进的 `- id: xxx` 行条目,嵌套 `name:` / `group:` / `isolate:` / `disabled:` / `config:` +// - `config:` 后跟标量、`|-` 字面块、或更深一层的 `- id:` 列表(group 场景) +// - `!!js` 标签原样保留为字符串 +// 不做通用 YAML 解析;遇到不支持的结构抛错并给出行号。 + +'use strict'; + +function linesOf(text) { + return text.replace(/\r\n/g, '\n').split('\n'); +} + +function indentOf(line) { + const m = /^[ ]*/.exec(line); + return m ? m[0].length : 0; +} + +function stripComment(line) { + // 只剥离行首为 '#' 的整行注释,行内注释保留(避免破坏 URL / 内容) + return line; +} + +function parseScalar(raw) { + const s = raw.trim(); + if (s === '') return ''; + if (s.startsWith('!!js')) return s; // 保留原样 + if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { + return s.slice(1, -1); + } + if (s === 'true') return true; + if (s === 'false') return false; + if (s === 'null' || s === '~') return null; + if (/^-?\d+(\.\d+)?$/.test(s)) return Number(s); + return s; +} + +// 解析一个字面块(`|-` / `|`),返回 { text, nextIndex } +function parseLiteralBlock(lines, startIndex, keyIndent) { + const out = []; + let i = startIndex; + for (; i < lines.length; i++) { + const line = lines[i]; + if (line.trim() === '') { + out.push(''); + continue; + } + const ind = indentOf(line); + if (ind <= keyIndent) break; + out.push(line.slice(Math.min(ind, keyIndent + 2)) === '' ? '' : line.slice(keyIndent + 2)); + } + // 去掉尾随空行 + while (out.length && out[out.length - 1] === '') out.pop(); + return { text: out.join('\n'), nextIndex: i }; +} + +// 解析一个条目块(从 `- id:` 行开始到下一个同级 `- id:` 之前) +function parseEntry(lines, startIndex) { + const entry = { id: '', name: '', disabled: undefined, group: false, isolate: undefined, config: undefined, raw: [] }; + let i = startIndex; + const keyIndent = indentOf(lines[startIndex]); + // 收集 raw(原样文本,用于无损复制) + for (let j = startIndex; j < lines.length; j++) { + const line = lines[j]; + if (j > startIndex && line.trim() !== '' && indentOf(line) <= keyIndent) break; + entry.raw.push(line); + } + let cursor = startIndex; + for (; cursor < lines.length; ) { + const line = lines[cursor]; + if (line.trim() === '' || line.trim().startsWith('#')) { + cursor++; + continue; + } + const ind = indentOf(line); + if (ind <= keyIndent && cursor > startIndex) break; + const trimmed = line.trim(); + if (trimmed.startsWith('- id:')) { + entry.id = trimmed.slice(5).trim(); + cursor++; + continue; + } + const m = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(trimmed); + if (!m) { + throw new Error(`yaml: 无法解析行 ${cursor + 1}: ${line}`); + } + const key = m[1]; + const rest = m[2]; + if (rest.startsWith('|-') || rest.startsWith('|')) { + const block = parseLiteralBlock(lines, cursor + 1, ind); + entry[key] = block.text; + cursor = block.nextIndex; + continue; + } + if (rest === '') { + // 键后跟更深缩进的嵌套块:整块按原文捕获;config 下的 `- id:` 列表做结构化解析 + let j = cursor + 1; + while (j < lines.length && (lines[j].trim() === '' || lines[j].trim().startsWith('#'))) j++; + if (j < lines.length && indentOf(lines[j]) > ind) { + if (key === 'config' && lines[j].trim().startsWith('- id:')) { + const nested = []; + let k = j; + let ok = true; + while (k < lines.length) { + const l = lines[k]; + if (l.trim() === '') { k++; continue; } + if (indentOf(l) <= ind) break; + if (l.trim().startsWith('- id:')) { + const sub = parseEntry(lines, k); + nested.push(sub); + k = sub.nextIndex; + } else { + ok = false; + break; + } + } + if (ok && nested.length) { + entry.config = { __rows: nested }; + cursor = k; + continue; + } + j = cursor + 1; + while (j < lines.length && (lines[j].trim() === '' || lines[j].trim().startsWith('#'))) j++; + } + const blockLines = []; + while (j < lines.length) { + const l = lines[j]; + if (l.trim() === '') { blockLines.push(''); j++; continue; } + if (indentOf(l) <= ind) break; + blockLines.push(l); + j++; + } + while (blockLines.length && blockLines[blockLines.length - 1] === '') blockLines.pop(); + entry[key] = blockLines.join('\n'); + cursor = j; + continue; + } + entry[key] = ''; + cursor = j; + continue; + } + entry[key] = parseScalar(rest); + cursor++; + } + entry.nextIndex = cursor; + return entry; +} + +// 解析 preset 目录中的两个 YAML 文件 +// 返回 { name, description, order, rows: [...], meta: { presetPath, compositionPath } } +function parsePreset(presetYamlText, compositionYamlText, paths = {}) { + const meta = { presetPath: paths.presetPath, compositionPath: paths.compositionPath }; + const presetInfo = { name: '', description: '', order: undefined }; + if (presetYamlText) { + for (const line of linesOf(presetYamlText)) { + const t = line.trim(); + if (t === '' || t.startsWith('#')) continue; + const m = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(t); + if (!m) continue; + if (m[1] === 'name' || m[1] === 'description' || m[1] === 'order') { + presetInfo[m[1]] = parseScalar(m[2]); + } + } + } + const rows = []; + if (compositionYamlText) { + const lines = linesOf(compositionYamlText); + let i = 0; + while (i < lines.length) { + const line = lines[i]; + const t = line.trim(); + if (t === '' || t.startsWith('#')) { i++; continue; } + if (t.startsWith('- id:')) { + const entry = parseEntry(lines, i); + rows.push(entry); + i = entry.nextIndex; + } else { + throw new Error(`yaml: 组合文件顶层只支持 '- id:' 条目,行 ${i + 1}: ${line}`); + } + } + } + return { ...presetInfo, rows, meta }; +} + +// ── 生成 ──────────────────────────────────────────────────────────────── + +function q(v) { + if (typeof v === 'string') { + if (v === '' || /[:#\[\]{}&*!|>'"%@`]/.test(v) || /^\s|\s$/.test(v)) { + return JSON.stringify(v); + } + return v; + } + return String(v); +} + +function indentBlock(text, pad) { + if (!text) return ''; + return text + .split('\n') + .map((l) => (l === '' ? '' : pad + l)) + .join('\n'); +} + +// 生成 preset.yml +function renderPresetYaml({ name, description, order = 90 }) { + const out = []; + out.push(`name: ${q(name)}`); + out.push(`description: ${q(description)}`); + if (order !== undefined) out.push(`order: ${order}`); + return out.join('\n') + '\n'; +} + +// 生成 agent.cordis.yml +// rows: [{ id, name, configText?, configRows?, configScalarList?, disabled? }] +// configScalarList: { key: string, items: string[] } —— 渲染为 key 下的标量列表(如 customSkillDirs 的 !!js 行) +// headerComment: 字符串或字符串数组 +function renderCompositionYaml(rows, headerComment) { + const out = []; + if (headerComment) { + const comments = Array.isArray(headerComment) ? headerComment : String(headerComment).split('\n'); + for (const l of comments) out.push('# ' + l); + out.push(''); + } + for (const row of rows) { + if (row.disabled === true) out.push(`- id: ${row.id}`); + else out.push(`- id: ${row.id}`); + out.push(` name: ${q(row.name)}`); + if (row.configText !== undefined && row.configText !== null) { + out.push(' config:'); + out.push(' text: |-'); + out.push(indentBlock(row.configText, ' ')); + } else if (row.configRows && row.configRows.length) { + out.push(' config:'); + for (const sub of row.configRows) { + out.push(` - id: ${sub.id}`); + if (sub.name) out.push(` name: ${q(sub.name)}`); + if (sub.configText !== undefined) { + out.push(' config:'); + out.push(' text: |-'); + out.push(indentBlock(sub.configText, ' ')); + } + } + } else if (row.configScalarList) { + out.push(' config:'); + out.push(` ${row.configScalarList.key}:`); + for (const item of row.configScalarList.items) { + out.push(` - ${item}`); + } + } else if (row.config !== undefined) { + out.push(` config: ${q(row.config)}`); + } + if (row.disabled === true) out.push(' disabled: true'); + } + return out.join('\n') + '\n'; +} + +module.exports = { parsePreset, renderPresetYaml, renderCompositionYaml, parseScalar, linesOf }; diff --git a/plugins/Fectivnfy112357/crossplug/plugin.json b/plugins/Fectivnfy112357/crossplug/plugin.json new file mode 100644 index 0000000..e48f0f1 --- /dev/null +++ b/plugins/Fectivnfy112357/crossplug/plugin.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "crossplug", + "version": "0.1.0", + "description": "Bidirectional plugin converter between DSH (DeepSeek Harness) and mcode (MiniMax Code / pi): convert DSH agent presets / plugin sources into mcode plugin packages (agent-plugins 1.0.0), and mcode / pi extensions into DSH presets or host plugins.", + "author": { + "name": "Fectivnfy112357", + "url": "https://github.com/Fectivnfy112357" + }, + "license": "Apache-2.0", + "keywords": ["dsh", "mcode", "converter", "bridge", "plugin"] +} diff --git a/plugins/Fectivnfy112357/crossplug/skills/convert-plugin/SKILL.md b/plugins/Fectivnfy112357/crossplug/skills/convert-plugin/SKILL.md new file mode 100644 index 0000000..f181fcc --- /dev/null +++ b/plugins/Fectivnfy112357/crossplug/skills/convert-plugin/SKILL.md @@ -0,0 +1,52 @@ +--- +name: convert-plugin +description: 在 DSH 与 mcode(MiniMax Code / pi)之间双向转换插件。当用户要求把 DSH 插件/preset 转成 mcode 插件、或把 mcode/pi 插件转成 DSH 插件时使用。 +--- + +# 插件转换(DSH ↔ mcode) + +本技能教 Agent 如何执行 DSH 与 mcode 之间的插件转换。转换核心(`core/run.js`)随本插件 +安装在插件目录的 `core/` 下,零 npm 依赖,全程本地执行(无网络访问、无凭据)。 + +## 何时使用 + +- 用户想把 **DSH** 的 agent preset 或插件源码搬到 mcode / MiniMax Code; +- 用户想把 **mcode / pi** 的 extension 或插件包搬到 DSH; +- 用户询问两侧已有哪些插件(`list`)。 + +## 转换命令 + +插件目录即本技能所在插件的根目录(MiniMax Code 的 Local 插件中显示为 crossplug): + +```text +node "<插件目录>/core/run.js" dsh2mcode [--out <目录>] +node "<插件目录>/core/run.js" mcode2dsh [--out <目录>] [--host] +node "<插件目录>/core/run.js" list --side dsh|mcode +``` + +`--host`:mcode2dsh 输出 DSH **宿主组合插件**(`lib/index.js`,所有会话可用、不依赖 preset 选择); +缺省输出 agent preset(`preset.yml` + `agent.cordis.yml` + `plugins/bridge.js` + `vendor/`)。 + +## 方向说明 + +### dsh2mcode(DSH → mcode) + +| 输入 | 输出 | +| --- | --- | +| DSH agent preset 目录(`preset.yml` + `agent.cordis.yml`) | **agent-plugins.org 1.0.0 插件包**(`plugin.json` + `skills/`),复制到 `~/.minimax/plugins//` 后经 `/plugins` → Local 安装;工具行按映射表分类,结论逐行写入 `CONVERSION-REPORT.md` | +| DSH 插件源码文件(.js 动态插件) | **agent-plugins MCP 包**(`plugin.json` + `mcp.json` + `mcp-server.js`),工具经 MCP stdio 生效——mcode CLI 用户侧唯一的插件工具路径 | + +### mcode2dsh(mcode → DSH) + +| 输入 | 输出 | +| --- | --- | +| pi extension 文件/包目录 | 桥接 preset 或 host 插件:原 extension 工厂经 pi-API 垫片原样执行(逻辑零移植),工具/命令/事件桥接注册为 DSH 能力 | +| mcode 插件包(`plugin.json` / `.minimax-plugin` / `.claude-plugin`) | 自包含 preset:persona + `skills/` 复制(`dsh-skill-filesystem` 挂载) | + +## 注意事项 + +- 转换是"脚手架 + 映射":无法映射的能力生成带说明的桩,每项结论在 `CONVERSION-REPORT.md`。 +- **mcode CLI 不加载 pi extension** :dsh2mcode 输出的 pi extension 包仅独立 pi CLI 有效, + 面向 mcode 的产物以 agent-plugins MCP 包为准;事件桥接(before_agent_start / turn_end)仅在 pi / DSH 运行时生效。 +- MCP 服务器不随 preset 迁移(DSH 侧需手工配置);技能内容经 `~/.agents/skills` 共享,不转换。 +- 转换结果安装后需刷新插件列表(`/plugins` 中 Ctrl+R)或重启相应工具。