@microi.net/cli 4.7.7 → 4.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +43 -0
- package/.mcp.json +11 -0
- package/LICENSE +21 -21
- package/README.md +18 -49
- package/assets/build-meta.json +16 -0
- package/assets/feature-matrix.json +129 -0
- package/assets/icon.png +0 -0
- package/assets/logo.svg +4 -0
- package/package.json +15 -9
- package/scripts/codex-marketplace.json +22 -0
- package/{dist → scripts}/mcp-server.js +3 -3
- package/{dist → scripts}/microi-cli.js +59 -52
- package/scripts/microi-codex-router.js +480 -0
- package/{dist → scripts}/microi-skills.meta.json +125 -119
- package/{dist/microi.skills → skills}/.microi-skills-version.json +2 -2
- package/{dist/microi.skills → skills}/README.md +4 -3
- package/{dist/microi.skills → skills}/dos-orm/SKILL.md +3 -0
- package/{dist/microi.skills → skills}/microi-client-frontend/SKILL.md +3 -3
- package/skills/microi-codex/SKILL.md +39 -0
- package/skills/microi-codex-installer/SKILL.md +97 -0
- package/skills/microi-codex-installer/agents/openai.yaml +7 -0
- package/{dist/microi.skills → skills}/microi-microservice/SKILL.md +8 -0
- package/{dist/microi.skills → skills}/module-engine/SKILL.md +3 -3
- package/{dist/microi.skills → skills}/module-engine/references/module-config.md +4 -4
- package/{dist/microi.skills → skills}/workspace-conventions/SKILL.md +6 -0
- /package/{dist → scripts}/mcp-codex-stdio-adapter.js +0 -0
- /package/{dist → scripts}/mcp-trae-windows-launcher.cmd +0 -0
- /package/{dist → scripts}/microi-cli-mcp.js +0 -0
- /package/{dist/microi.skills → skills}/ai-engine/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/ai-engine/agents/openai.yaml +0 -0
- /package/{dist/microi.skills → skills}/app-store/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/app-store/agents/openai.yaml +0 -0
- /package/{dist/microi.skills → skills}/business-blueprint/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/datasource-engine/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/datasource-engine/agents/openai.yaml +0 -0
- /package/{dist/microi.skills → skills}/dos-orm/references/api-reference.md +0 -0
- /package/{dist/microi.skills → skills}/job-engine/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/job-engine/agents/openai.yaml +0 -0
- /package/{dist/microi.skills → skills}/message-notification/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/message-notification/agents/openai.yaml +0 -0
- /package/{dist/microi.skills → skills}/message-notification/references/contracts.md +0 -0
- /package/{dist/microi.skills → skills}/microi-ai-app-auth.js +0 -0
- /package/{dist/microi.skills → skills}/microi-ai-application/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/microi-ai-application/agents/openai.yaml +0 -0
- /package/{dist/microi.skills → skills}/microi-ai-application/references/frontend-baseline.md +0 -0
- /package/{dist/microi.skills → skills}/microi-datasource-mapping/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/microi-db-schema/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/microi-db-schema/agents/openai.yaml +0 -0
- /package/{dist/microi.skills → skills}/microi-db-schema/references/core-tables.md +0 -0
- /package/{dist/microi.skills → skills}/microi-db-schema/references/form-component-options.md +0 -0
- /package/{dist/microi.skills → skills}/microi-db-schema/references/schema-overview.md +0 -0
- /package/{dist/microi.skills → skills}/microi-db-schema/references/schema.md +0 -0
- /package/{dist/microi.skills → skills}/microi-db-schema/references/table-catalog.md +0 -0
- /package/{dist/microi.skills → skills}/microi-deployment/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/microi-deployment/references/deployment-matrix.md +0 -0
- /package/{dist/microi.skills → skills}/microi-docs-coverage/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/microi-docs-coverage/references/capability-map.md +0 -0
- /package/{dist/microi.skills → skills}/microi-docs-coverage/scripts/audit-doc-skill-coverage.mjs +0 -0
- /package/{dist/microi.skills → skills}/microi-form-engine/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/microi-form-engine/references/component-catalog.md +0 -0
- /package/{dist/microi.skills → skills}/microi-form-engine/references/data-source-events.md +0 -0
- /package/{dist/microi.skills → skills}/microi-form-layout/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/microi-frontend-sdk/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/microi-left-right-layout/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/microi-microservice/references/runtime-delivery.md +0 -0
- /package/{dist/microi.skills → skills}/microi-mobile-app-quality/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/microi-solution-quotation/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/microi-solution-quotation/agents/openai.yaml +0 -0
- /package/{dist/microi.skills → skills}/microi-solution-quotation/scripts/build_solution_quote.py +0 -0
- /package/{dist/microi.skills → skills}/microi-system-delivery/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/microi-ui/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/microi-uniapp-frontend/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/microi.v8.js +0 -0
- /package/{dist/microi.skills → skills}/ocr-engine/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/ocr-engine/agents/openai.yaml +0 -0
- /package/{dist/microi.skills → skills}/page-engine/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/performance-testing/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/playwright-e2e/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/print-engine/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/production-readonly-audit/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/report-engine/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/report-engine/agents/openai.yaml +0 -0
- /package/{dist/microi.skills → skills}/search-engine/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/search-engine/agents/openai.yaml +0 -0
- /package/{dist/microi.skills → skills}/spider-engine/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/translate-engine/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/translate-engine/agents/openai.yaml +0 -0
- /package/{dist/microi.skills → skills}/ui-design/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/ui-design/assets/pattern-showcase/app.js +0 -0
- /package/{dist/microi.skills → skills}/ui-design/assets/pattern-showcase/index.html +0 -0
- /package/{dist/microi.skills → skills}/ui-design/assets/pattern-showcase/styles.css +0 -0
- /package/{dist/microi.skills → skills}/ui-design/assets/templates/MCI-DESIGN.md +0 -0
- /package/{dist/microi.skills → skills}/ui-design/references/design-pattern-library.md +0 -0
- /package/{dist/microi.skills → skills}/ui-design/references/mci-design-contract.md +0 -0
- /package/{dist/microi.skills → skills}/ui-design/references/motion-and-media.md +0 -0
- /package/{dist/microi.skills → skills}/ui-design/references/product-flow-recipes.md +0 -0
- /package/{dist/microi.skills → skills}/uniapp-mall-assets/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/v8-api-config/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/v8-cache-pattern/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/v8-crud-api/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/v8-debugging/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/v8-explorer-tree/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/v8-export-import/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/v8-file-upload/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/v8-formengine-http/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/v8-frontend-events/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/v8-frontend-events/references/bluetooth-print-api.md +0 -0
- /package/{dist/microi.skills → skills}/v8-frontend-events/references/bluetooth-print.md +0 -0
- /package/{dist/microi.skills → skills}/v8-http-integration/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/v8-image-processing/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/v8-image-processing/agents/openai.yaml +0 -0
- /package/{dist/microi.skills → skills}/v8-image-processing/references/api-reference.md +0 -0
- /package/{dist/microi.skills → skills}/v8-menu-buttons/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/v8-mongodb/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/v8-mq-mqtt/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/v8-saas-multi-tenant/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/v8-security/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/v8-sql-query/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/v8-table-event/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/v8-template-engine/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/v8-utilities/SKILL.md +0 -0
- /package/{dist/microi.skills → skills}/v8-utilities/references/client-api-index.md +0 -0
- /package/{dist/microi.skills → skills}/v8-utilities/references/platform-http-routes.md +0 -0
- /package/{dist/microi.skills → skills}/v8-utilities/references/server-api-index.md +0 -0
- /package/{dist/microi.skills → skills}/v8-workflow/SKILL.md +0 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* Microi Codex Plugin 多连接路由器。
|
|
4
|
+
*
|
|
5
|
+
* 它只负责发现 VS Code/CLI 共用的工作区配置、选择连接并把请求转发给
|
|
6
|
+
* 同一份 Microi MCP bundle。所有业务 schema、确认口令、审计和写后回读
|
|
7
|
+
* 都由原 MCP 工具执行,本文件不实现第二套远端 API。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const childProcess = require('node:child_process');
|
|
11
|
+
const crypto = require('node:crypto');
|
|
12
|
+
const fs = require('node:fs');
|
|
13
|
+
const os = require('node:os');
|
|
14
|
+
const path = require('node:path');
|
|
15
|
+
|
|
16
|
+
const CONFIG_DIR = 'Microi-V8-Engine';
|
|
17
|
+
const CONFIG_FILE = '.microi-config.json';
|
|
18
|
+
const TOKEN_FILE = '.microi-mcp-tokens.json';
|
|
19
|
+
const PROTOCOL_VERSION = '2024-11-05';
|
|
20
|
+
const pluginRoot = path.resolve(__dirname, '..');
|
|
21
|
+
const mcpServerPath = path.join(__dirname, 'mcp-server.js');
|
|
22
|
+
const packagePath = path.join(pluginRoot, 'package.json');
|
|
23
|
+
const pluginVersion = readJson(packagePath, {}).version || '0.0.0';
|
|
24
|
+
const children = new Map();
|
|
25
|
+
let inputBuffer = '';
|
|
26
|
+
let requestSequence = 0;
|
|
27
|
+
let shuttingDown = false;
|
|
28
|
+
|
|
29
|
+
function readJson(filePath, fallback) {
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
32
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : fallback;
|
|
33
|
+
} catch {
|
|
34
|
+
return fallback;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeUrl(value) {
|
|
39
|
+
return String(value || '').trim().replace(/\/+$/, '');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function safeName(value) {
|
|
43
|
+
return String(value || '').normalize('NFKD').replace(/[^\x00-\x7F]/g, '')
|
|
44
|
+
.replace(/[^A-Za-z0-9_]+/g, '_').replace(/_+/g, '_')
|
|
45
|
+
.replace(/^_+|_+$/g, '').toLowerCase().slice(0, 48);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function hostPart(profile) {
|
|
49
|
+
try { return safeName(new URL(profile.apiBaseUrl).host); } catch { return safeName(profile.apiBaseUrl); }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function profileIdentity(profile) {
|
|
53
|
+
return [
|
|
54
|
+
normalizeUrl(profile.apiBaseUrl),
|
|
55
|
+
String(profile.osClient || '').trim(),
|
|
56
|
+
String(profile.osClientType || '').trim(),
|
|
57
|
+
String(profile.osClientNetwork || '').trim(),
|
|
58
|
+
].join('|');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function candidateRoots() {
|
|
62
|
+
const values = [
|
|
63
|
+
process.env.MICROI_WORKSPACE_ROOT,
|
|
64
|
+
process.env.CODEX_WORKSPACE_ROOT,
|
|
65
|
+
process.env.INIT_CWD,
|
|
66
|
+
process.cwd(),
|
|
67
|
+
].filter(Boolean).map(value => path.resolve(value));
|
|
68
|
+
const roots = [];
|
|
69
|
+
for (const start of values) {
|
|
70
|
+
let current = start;
|
|
71
|
+
while (current && !roots.includes(current)) {
|
|
72
|
+
roots.push(current);
|
|
73
|
+
const parent = path.dirname(current);
|
|
74
|
+
if (parent === current) break;
|
|
75
|
+
current = parent;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return roots;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolveWorkspace() {
|
|
82
|
+
const explicitConfig = process.env.MICROI_CONFIG_PATH
|
|
83
|
+
? path.resolve(process.env.MICROI_CONFIG_PATH)
|
|
84
|
+
: '';
|
|
85
|
+
const candidates = explicitConfig
|
|
86
|
+
? [explicitConfig]
|
|
87
|
+
: candidateRoots().flatMap(root => [
|
|
88
|
+
path.join(root, CONFIG_DIR, CONFIG_FILE),
|
|
89
|
+
path.join(root, CONFIG_DIR.toLowerCase(), CONFIG_FILE),
|
|
90
|
+
]);
|
|
91
|
+
const configPath = candidates.find(filePath => fs.existsSync(filePath));
|
|
92
|
+
const fallbackRoot = path.resolve(process.env.MICROI_WORKSPACE_ROOT || process.cwd());
|
|
93
|
+
if (!configPath) {
|
|
94
|
+
return {
|
|
95
|
+
workspaceRoot: fallbackRoot,
|
|
96
|
+
syncRoot: path.join(fallbackRoot, CONFIG_DIR),
|
|
97
|
+
configPath: path.join(fallbackRoot, CONFIG_DIR, CONFIG_FILE),
|
|
98
|
+
tokenPath: path.join(fallbackRoot, CONFIG_DIR, TOKEN_FILE),
|
|
99
|
+
config: {},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const syncRoot = path.dirname(configPath);
|
|
103
|
+
return {
|
|
104
|
+
workspaceRoot: path.dirname(syncRoot),
|
|
105
|
+
syncRoot,
|
|
106
|
+
configPath,
|
|
107
|
+
tokenPath: path.join(syncRoot, TOKEN_FILE),
|
|
108
|
+
config: readJson(configPath, {}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function loadProfiles() {
|
|
113
|
+
const workspace = resolveWorkspace();
|
|
114
|
+
const configured = Array.isArray(workspace.config.profiles)
|
|
115
|
+
? workspace.config.profiles
|
|
116
|
+
: [];
|
|
117
|
+
const rawProfiles = configured.length > 0
|
|
118
|
+
? configured
|
|
119
|
+
: (workspace.config.apiBaseUrl ? [{
|
|
120
|
+
label: workspace.config.osClient || workspace.config.apiBaseUrl,
|
|
121
|
+
apiBaseUrl: workspace.config.apiBaseUrl,
|
|
122
|
+
osClient: workspace.config.osClient || '',
|
|
123
|
+
}] : []);
|
|
124
|
+
const tokens = readJson(workspace.tokenPath, {});
|
|
125
|
+
const identityNames = rawProfiles.map(profile => safeName(profile.osClient || profile.label) || hostPart(profile) || 'default');
|
|
126
|
+
const counts = identityNames.reduce((map, name) => map.set(name, (map.get(name) || 0) + 1), new Map());
|
|
127
|
+
const used = new Set();
|
|
128
|
+
const profiles = rawProfiles.filter(profile => /^https?:\/\//i.test(String(profile.apiBaseUrl || ''))).map((raw, index) => {
|
|
129
|
+
const profile = {
|
|
130
|
+
label: String(raw.label || raw.sysShortTitle || raw.sysTitle || raw.osClient || raw.apiBaseUrl || ''),
|
|
131
|
+
apiBaseUrl: normalizeUrl(raw.apiBaseUrl),
|
|
132
|
+
osClient: String(raw.osClient || '').trim(),
|
|
133
|
+
osClientType: String(raw.osClientType || '').trim(),
|
|
134
|
+
osClientNetwork: String(raw.osClientNetwork || '').trim(),
|
|
135
|
+
};
|
|
136
|
+
const identityName = identityNames[index];
|
|
137
|
+
const host = hostPart(profile);
|
|
138
|
+
const base = `microi_${identityName}`;
|
|
139
|
+
let name = counts.get(identityName) > 1 && host && host !== identityName ? `${base}_${host}` : base;
|
|
140
|
+
if (used.has(name) && host) name = `${base}_${host}`;
|
|
141
|
+
let suffix = 2;
|
|
142
|
+
while (used.has(name)) name = `${base}_${suffix++}`;
|
|
143
|
+
used.add(name);
|
|
144
|
+
const exactKey = profileIdentity(profile);
|
|
145
|
+
const legacyKey = profile.osClient ? `${profile.apiBaseUrl}|${profile.osClient}` : profile.apiBaseUrl;
|
|
146
|
+
return {
|
|
147
|
+
...profile,
|
|
148
|
+
index: index + 1,
|
|
149
|
+
name,
|
|
150
|
+
identity: exactKey,
|
|
151
|
+
loggedIn: typeof tokens[exactKey] === 'string' || typeof tokens[legacyKey] === 'string',
|
|
152
|
+
workspaceRoot: workspace.workspaceRoot,
|
|
153
|
+
syncRoot: workspace.syncRoot,
|
|
154
|
+
tokenPath: workspace.tokenPath,
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
return { workspace, profiles };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function publicProfile(profile) {
|
|
161
|
+
return {
|
|
162
|
+
index: profile.index,
|
|
163
|
+
name: profile.name,
|
|
164
|
+
label: profile.label,
|
|
165
|
+
apiBaseUrl: profile.apiBaseUrl,
|
|
166
|
+
osClient: profile.osClient,
|
|
167
|
+
osClientType: profile.osClientType,
|
|
168
|
+
osClientNetwork: profile.osClientNetwork,
|
|
169
|
+
loggedIn: profile.loggedIn,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function selectProfile(selector) {
|
|
174
|
+
const state = loadProfiles();
|
|
175
|
+
if (state.profiles.length === 0) {
|
|
176
|
+
throw new Error(`当前工作区尚未初始化 Microi。请在 ${state.workspace.workspaceRoot} 运行 bundled CLI 的 init 命令。`);
|
|
177
|
+
}
|
|
178
|
+
const raw = String(selector || '').trim();
|
|
179
|
+
if (raw) {
|
|
180
|
+
const lower = raw.toLowerCase();
|
|
181
|
+
const matches = state.profiles.filter(profile =>
|
|
182
|
+
String(profile.index) === raw
|
|
183
|
+
|| profile.name.toLowerCase() === lower
|
|
184
|
+
|| profile.osClient.toLowerCase() === lower
|
|
185
|
+
|| profile.label.toLowerCase() === lower
|
|
186
|
+
|| profile.identity.toLowerCase() === lower
|
|
187
|
+
);
|
|
188
|
+
if (matches.length === 1) return matches[0];
|
|
189
|
+
if (matches.length > 1) throw new Error(`连接选择“${raw}”存在歧义,请改用 profiles 返回的 name。`);
|
|
190
|
+
throw new Error(`未找到连接“${raw}”。可用连接:${state.profiles.map(item => item.name).join(', ')}`);
|
|
191
|
+
}
|
|
192
|
+
const loggedIn = state.profiles.filter(profile => profile.loggedIn);
|
|
193
|
+
if (loggedIn.length === 1) return loggedIn[0];
|
|
194
|
+
if (state.profiles.length === 1) return state.profiles[0];
|
|
195
|
+
throw new Error(`当前工作区有 ${state.profiles.length} 个 Microi 连接,请先调用 action="profiles",再传 profile=name。`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function buildApplicationsDir(profile) {
|
|
199
|
+
const title = profile.label || profile.apiBaseUrl || 'Microi';
|
|
200
|
+
const serverDir = `${title} (${hostPart(profile) || 'server'})`.replace(/[<>:"/\\|?*]/g, '_');
|
|
201
|
+
const osClientDir = [profile.osClient || 'default', profile.osClientType || 'default', profile.osClientNetwork || 'default']
|
|
202
|
+
.join('.').replace(/[<>:"/\\|?*]/g, '_');
|
|
203
|
+
return path.join(profile.syncRoot, serverDir, osClientDir, 'AI应用');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function deviceId() {
|
|
207
|
+
const raw = `MCP:${os.hostname() || 'Unknown'}`;
|
|
208
|
+
if (/^[\x20-\x7E]{1,128}$/.test(raw)) return raw;
|
|
209
|
+
return `MCP-${crypto.createHash('sha256').update(raw).digest('hex').slice(0, 16)}`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
class ChildMcp {
|
|
213
|
+
constructor(profile) {
|
|
214
|
+
this.profile = profile;
|
|
215
|
+
this.pending = new Map();
|
|
216
|
+
this.buffer = '';
|
|
217
|
+
this.ready = this.start();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async start() {
|
|
221
|
+
if (!fs.existsSync(mcpServerPath)) {
|
|
222
|
+
throw new Error(`Codex Plugin 缺少 MCP bundle:${mcpServerPath}`);
|
|
223
|
+
}
|
|
224
|
+
this.child = childProcess.spawn(process.execPath, [mcpServerPath], {
|
|
225
|
+
cwd: this.profile.workspaceRoot,
|
|
226
|
+
windowsHide: true,
|
|
227
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
228
|
+
env: {
|
|
229
|
+
...process.env,
|
|
230
|
+
MICROI_API_URL: this.profile.apiBaseUrl,
|
|
231
|
+
MICROI_TOKEN_FILE: this.profile.tokenPath,
|
|
232
|
+
MICROI_AUTH_RECOVERY_DIR: `${this.profile.tokenPath}.recovery`,
|
|
233
|
+
MICROI_OS_CLIENT: this.profile.osClient,
|
|
234
|
+
MICROI_OS_CLIENT_TYPE: this.profile.osClientType,
|
|
235
|
+
MICROI_OS_CLIENT_NETWORK: this.profile.osClientNetwork,
|
|
236
|
+
MICROI_WORKSPACE_ROOT: this.profile.workspaceRoot,
|
|
237
|
+
MICROI_SYNC_ROOT: this.profile.syncRoot,
|
|
238
|
+
MICROI_AI_APPLICATIONS_DIR: buildApplicationsDir(this.profile),
|
|
239
|
+
MICROI_LABEL_BASE64: Buffer.from(this.profile.label, 'utf8').toString('base64'),
|
|
240
|
+
MICROI_MCP_DID: deviceId(),
|
|
241
|
+
MICROI_TOOL_SOURCE: 'codex-plugin',
|
|
242
|
+
MICROI_TOOL_VERSION: pluginVersion,
|
|
243
|
+
MICROI_CODEX_MODE: '1',
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
this.child.stdout.on('data', chunk => this.onData(chunk));
|
|
247
|
+
this.child.stderr.on('data', chunk => process.stderr.write(`[${this.profile.name}] ${chunk}`));
|
|
248
|
+
this.child.on('error', error => this.rejectAll(error));
|
|
249
|
+
this.child.on('exit', code => this.rejectAll(new Error(`Microi MCP ${this.profile.name} 已退出(${code ?? 'unknown'})`)));
|
|
250
|
+
await this.request('initialize', {
|
|
251
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
252
|
+
capabilities: {},
|
|
253
|
+
clientInfo: { name: 'microi-codex-plugin-router', version: pluginVersion },
|
|
254
|
+
}, true);
|
|
255
|
+
this.notify('notifications/initialized', {});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
onData(chunk) {
|
|
259
|
+
this.buffer += chunk.toString('utf8');
|
|
260
|
+
let newline;
|
|
261
|
+
while ((newline = this.buffer.indexOf('\n')) >= 0) {
|
|
262
|
+
const line = this.buffer.slice(0, newline).trim();
|
|
263
|
+
this.buffer = this.buffer.slice(newline + 1);
|
|
264
|
+
if (!line) continue;
|
|
265
|
+
let message;
|
|
266
|
+
try { message = JSON.parse(line); } catch { continue; }
|
|
267
|
+
if (message.id == null) continue;
|
|
268
|
+
const pending = this.pending.get(String(message.id));
|
|
269
|
+
if (!pending) continue;
|
|
270
|
+
this.pending.delete(String(message.id));
|
|
271
|
+
if (message.error) pending.reject(new Error(message.error.message || JSON.stringify(message.error)));
|
|
272
|
+
else pending.resolve(message.result);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
rejectAll(error) {
|
|
277
|
+
for (const pending of this.pending.values()) pending.reject(error);
|
|
278
|
+
this.pending.clear();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
write(message) {
|
|
282
|
+
if (!this.child || this.child.stdin.destroyed) throw new Error(`Microi MCP ${this.profile.name} 不可用`);
|
|
283
|
+
this.child.stdin.write(`${JSON.stringify(message)}\n`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
request(method, params, skipReady = false) {
|
|
287
|
+
return (async () => {
|
|
288
|
+
if (!skipReady) await this.ready;
|
|
289
|
+
const id = `router-${process.pid}-${++requestSequence}`;
|
|
290
|
+
const response = new Promise((resolve, reject) => this.pending.set(id, { resolve, reject }));
|
|
291
|
+
this.write({ jsonrpc: '2.0', id, method, params });
|
|
292
|
+
return response;
|
|
293
|
+
})();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
notify(method, params) {
|
|
297
|
+
this.write({ jsonrpc: '2.0', method, params });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
stop() {
|
|
301
|
+
try { this.child?.stdin.end(); } catch { /* best effort */ }
|
|
302
|
+
try { this.child?.kill('SIGTERM'); } catch { /* best effort */ }
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function childFor(profile) {
|
|
307
|
+
const existing = children.get(profile.identity);
|
|
308
|
+
if (existing) return existing;
|
|
309
|
+
const child = new ChildMcp(profile);
|
|
310
|
+
children.set(profile.identity, child);
|
|
311
|
+
return child;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function resultText(value, isError = false) {
|
|
315
|
+
return {
|
|
316
|
+
content: [{ type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value, null, 2) }],
|
|
317
|
+
...(isError ? { isError: true } : {}),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function toolCatalog() {
|
|
322
|
+
return [{
|
|
323
|
+
name: 'microi_codex',
|
|
324
|
+
description: 'Microi吾码统一入口。先用 action="profiles" 查看当前工作区连接;多连接时传 profile=name。其余 action 为 list_tools、describe_tool 或任意原始 microi_* 工具,全部复用原工具验证、确认、审计和回读。',
|
|
325
|
+
inputSchema: {
|
|
326
|
+
type: 'object',
|
|
327
|
+
additionalProperties: false,
|
|
328
|
+
required: ['action'],
|
|
329
|
+
properties: {
|
|
330
|
+
profile: { type: 'string', description: '可选:profiles 返回的 name、序号、OsClient 或连接名称。多连接时必填。' },
|
|
331
|
+
action: { type: 'string', description: 'profiles | list_tools | describe_tool | 任意原始 microi_* 工具名' },
|
|
332
|
+
params: { type: 'object', additionalProperties: true, description: '所选 action 的参数。' },
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
}];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function callRouterTool(args) {
|
|
339
|
+
const action = String(args?.action || '').trim();
|
|
340
|
+
if (!action) return resultText('action 不能为空', true);
|
|
341
|
+
if (action === 'profiles' || action === 'profile_list') {
|
|
342
|
+
const state = loadProfiles();
|
|
343
|
+
return resultText({
|
|
344
|
+
workspace: state.workspace.workspaceRoot,
|
|
345
|
+
configPath: state.workspace.configPath,
|
|
346
|
+
count: state.profiles.length,
|
|
347
|
+
profiles: state.profiles.map(publicProfile),
|
|
348
|
+
next: state.profiles.length > 1 ? '后续调用请传 profile=name。' : '后续调用可省略 profile。',
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
const profile = selectProfile(args?.profile);
|
|
353
|
+
return await childFor(profile).request('tools/call', {
|
|
354
|
+
name: 'microi_codex',
|
|
355
|
+
arguments: { action, params: args?.params || {} },
|
|
356
|
+
});
|
|
357
|
+
} catch (error) {
|
|
358
|
+
return resultText(`Microi Codex 路由失败:${error instanceof Error ? error.message : String(error)}`, true);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function resourceList() {
|
|
363
|
+
const state = loadProfiles();
|
|
364
|
+
return [{
|
|
365
|
+
uri: 'microi://codex/profiles',
|
|
366
|
+
name: 'microi_profiles',
|
|
367
|
+
title: 'Microi connection profiles',
|
|
368
|
+
description: '当前工作区 Microi 连接与登录状态。',
|
|
369
|
+
mimeType: 'application/json',
|
|
370
|
+
}, ...state.profiles.flatMap(profile => ['status', 'tools'].map(kind => ({
|
|
371
|
+
uri: `microi://codex/${encodeURIComponent(profile.name)}/${kind}`,
|
|
372
|
+
name: `microi_${profile.name}_${kind}`,
|
|
373
|
+
title: `Microi ${profile.label || profile.name} ${kind}`,
|
|
374
|
+
description: kind === 'status' ? '只读连接状态。' : '该连接的原始工具目录。',
|
|
375
|
+
mimeType: 'application/json',
|
|
376
|
+
})))];
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function readResource(uri) {
|
|
380
|
+
if (uri === 'microi://codex/profiles') {
|
|
381
|
+
const state = loadProfiles();
|
|
382
|
+
return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify({ profiles: state.profiles.map(publicProfile) }, null, 2) }] };
|
|
383
|
+
}
|
|
384
|
+
const match = /^microi:\/\/codex\/([^/]+)\/(status|tools|action\/([^/]+)\/(.*))$/.exec(uri);
|
|
385
|
+
if (!match) throw new Error(`不支持的 Microi 资源 URI:${uri}`);
|
|
386
|
+
const profile = selectProfile(decodeURIComponent(match[1]));
|
|
387
|
+
let action;
|
|
388
|
+
let params = {};
|
|
389
|
+
if (match[2] === 'status') action = 'microi_get_status';
|
|
390
|
+
else if (match[2] === 'tools') action = 'list_tools';
|
|
391
|
+
else {
|
|
392
|
+
action = decodeURIComponent(match[3]);
|
|
393
|
+
const raw = decodeURIComponent(match[4] || '');
|
|
394
|
+
params = raw ? JSON.parse(raw) : {};
|
|
395
|
+
}
|
|
396
|
+
const result = await childFor(profile).request('tools/call', {
|
|
397
|
+
name: 'microi_codex',
|
|
398
|
+
arguments: { action, params },
|
|
399
|
+
});
|
|
400
|
+
return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(result, null, 2) }] };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function send(message) {
|
|
404
|
+
process.stdout.write(`${JSON.stringify(message)}\n`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function sendResult(id, result) {
|
|
408
|
+
send({ jsonrpc: '2.0', id, result });
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function sendError(id, code, message) {
|
|
412
|
+
send({ jsonrpc: '2.0', id, error: { code, message } });
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async function handle(message) {
|
|
416
|
+
if (!message || message.jsonrpc !== '2.0') return;
|
|
417
|
+
const { id, method, params } = message;
|
|
418
|
+
if (method === 'initialize') {
|
|
419
|
+
sendResult(id, {
|
|
420
|
+
protocolVersion: params?.protocolVersion || PROTOCOL_VERSION,
|
|
421
|
+
capabilities: { tools: {}, resources: {} },
|
|
422
|
+
serverInfo: { name: 'Microi-Codex-Plugin', version: pluginVersion },
|
|
423
|
+
instructions: '先调用 microi_codex action="profiles";多连接时把返回的 name 作为 profile。所有业务动作由同源 Microi MCP 工具处理。',
|
|
424
|
+
});
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (method === 'ping') { sendResult(id, {}); return; }
|
|
428
|
+
if (method === 'tools/list') { sendResult(id, { tools: toolCatalog() }); return; }
|
|
429
|
+
if (method === 'tools/call') {
|
|
430
|
+
if (params?.name !== 'microi_codex') { sendResult(id, resultText(`Unknown tool: ${params?.name || '(empty)'}`, true)); return; }
|
|
431
|
+
sendResult(id, await callRouterTool(params.arguments || {})); return;
|
|
432
|
+
}
|
|
433
|
+
if (method === 'resources/list') { sendResult(id, { resources: resourceList() }); return; }
|
|
434
|
+
if (method === 'resources/templates/list') {
|
|
435
|
+
sendResult(id, { resourceTemplates: [{
|
|
436
|
+
uriTemplate: 'microi://codex/{profile}/action/{action}/{params}',
|
|
437
|
+
name: 'microi_codex_action',
|
|
438
|
+
title: 'Microi action fallback',
|
|
439
|
+
description: '工具未注入时,按连接路由到同一原始 Microi 工具。params 是 URI 编码 JSON 对象。',
|
|
440
|
+
mimeType: 'application/json',
|
|
441
|
+
}] });
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if (method === 'resources/read') {
|
|
445
|
+
try { sendResult(id, await readResource(String(params?.uri || ''))); }
|
|
446
|
+
catch (error) { sendError(id, -32002, error instanceof Error ? error.message : String(error)); }
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (method === 'shutdown') { sendResult(id, {}); shutdown(); return; }
|
|
450
|
+
if (method?.startsWith('notifications/')) return;
|
|
451
|
+
if (id != null) sendError(id, -32601, `Method not found: ${method}`);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function shutdown() {
|
|
455
|
+
if (shuttingDown) return;
|
|
456
|
+
shuttingDown = true;
|
|
457
|
+
for (const child of children.values()) child.stop();
|
|
458
|
+
children.clear();
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
process.stdin.setEncoding('utf8');
|
|
462
|
+
process.stdin.on('data', chunk => {
|
|
463
|
+
inputBuffer += chunk;
|
|
464
|
+
let newline;
|
|
465
|
+
while ((newline = inputBuffer.indexOf('\n')) >= 0) {
|
|
466
|
+
const line = inputBuffer.slice(0, newline).trim();
|
|
467
|
+
inputBuffer = inputBuffer.slice(newline + 1);
|
|
468
|
+
if (!line) continue;
|
|
469
|
+
let message;
|
|
470
|
+
try { message = JSON.parse(line); }
|
|
471
|
+
catch { continue; }
|
|
472
|
+
Promise.resolve(handle(message)).catch(error => {
|
|
473
|
+
if (message.id != null) sendError(message.id, -32603, error instanceof Error ? error.message : String(error));
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
process.stdin.on('end', () => { shutdown(); process.exit(0); });
|
|
478
|
+
process.stdin.on('error', () => { shutdown(); process.exit(1); });
|
|
479
|
+
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.on(signal, () => { shutdown(); process.exit(0); });
|
|
480
|
+
process.on('exit', shutdown);
|