@minko-ai/minko 0.1.0 → 0.1.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/README.md +577 -2
- package/assets/mascot-preview.png +0 -0
- package/assets/mascot-preview.txt +46 -0
- package/assets/mascot-source.png +0 -0
- package/assets/skills/README.md +21 -0
- package/assets/skills/brainstorming/LICENSE +21 -0
- package/assets/skills/brainstorming/SKILL.md +316 -0
- package/assets/skills/buddhist-method/LICENSE +21 -0
- package/assets/skills/buddhist-method/SKILL.md +137 -0
- package/assets/skills/buddhist-method/references/ariyasacca-debug.md +57 -0
- package/assets/skills/buddhist-method/references/extended-principles.md +100 -0
- package/assets/skills/code-humanizer/LICENSE +21 -0
- package/assets/skills/code-humanizer/SKILL.md +174 -0
- package/assets/skills/code-review/LICENSE +21 -0
- package/assets/skills/code-review/SKILL.md +232 -0
- package/assets/skills/code-review/reference/angular.md +768 -0
- package/assets/skills/code-review/reference/architecture-review-guide.md +472 -0
- package/assets/skills/code-review/reference/c.md +890 -0
- package/assets/skills/code-review/reference/code-quality-universal.md +488 -0
- package/assets/skills/code-review/reference/code-review-best-practices.md +136 -0
- package/assets/skills/code-review/reference/common-bugs-checklist.md +302 -0
- package/assets/skills/code-review/reference/cpp.md +893 -0
- package/assets/skills/code-review/reference/cross-cutting/async-concurrency-patterns.md +515 -0
- package/assets/skills/code-review/reference/cross-cutting/error-handling-principles.md +492 -0
- package/assets/skills/code-review/reference/cross-cutting/n-plus-one-queries.md +309 -0
- package/assets/skills/code-review/reference/cross-cutting/sql-injection-prevention.md +308 -0
- package/assets/skills/code-review/reference/cross-cutting/xss-prevention.md +264 -0
- package/assets/skills/code-review/reference/csharp.md +525 -0
- package/assets/skills/code-review/reference/css-less-sass.md +661 -0
- package/assets/skills/code-review/reference/dart.md +670 -0
- package/assets/skills/code-review/reference/django.md +985 -0
- package/assets/skills/code-review/reference/fastapi.md +580 -0
- package/assets/skills/code-review/reference/go.md +993 -0
- package/assets/skills/code-review/reference/java.md +409 -0
- package/assets/skills/code-review/reference/java8.md +586 -0
- package/assets/skills/code-review/reference/kotlin.md +1018 -0
- package/assets/skills/code-review/reference/nestjs.md +593 -0
- package/assets/skills/code-review/reference/performance-review-guide.md +816 -0
- package/assets/skills/code-review/reference/php.md +684 -0
- package/assets/skills/code-review/reference/python.md +1073 -0
- package/assets/skills/code-review/reference/qt.md +757 -0
- package/assets/skills/code-review/reference/react.md +871 -0
- package/assets/skills/code-review/reference/ruby.md +964 -0
- package/assets/skills/code-review/reference/rust.md +846 -0
- package/assets/skills/code-review/reference/security-review-guide.md +494 -0
- package/assets/skills/code-review/reference/svelte.md +1064 -0
- package/assets/skills/code-review/reference/swift.md +936 -0
- package/assets/skills/code-review/reference/typescript.md +1016 -0
- package/assets/skills/code-review/reference/vue.md +924 -0
- package/assets/skills/code-review/reference/zig.md +440 -0
- package/assets/skills/cyclomatic-complexity/LICENSE +201 -0
- package/assets/skills/cyclomatic-complexity/SKILL.md +69 -0
- package/assets/skills/five-whys/LICENSE +21 -0
- package/assets/skills/five-whys/SKILL.md +247 -0
- package/assets/skills/five-whys/examples/performance-regression.md +161 -0
- package/assets/skills/five-whys/examples/production-incident.md +134 -0
- package/assets/skills/five-whys/references/software-patterns.md +284 -0
- package/assets/skills/five-whys/references/toyota-origins.md +257 -0
- package/assets/skills/systematic-debugging/LICENSE +21 -0
- package/assets/skills/systematic-debugging/SKILL.md +283 -0
- package/assets/skills/test-driven-development/LICENSE +21 -0
- package/assets/skills/test-driven-development/SKILL.md +330 -0
- package/assets/skills/verification-before-completion/LICENSE +21 -0
- package/assets/skills/verification-before-completion/SKILL.md +120 -0
- package/assets/skills/vibe-sec/LICENSE +201 -0
- package/assets/skills/vibe-sec/SKILL.md +758 -0
- package/assets/skills/writing-plans/LICENSE +21 -0
- package/assets/skills/writing-plans/SKILL.md +204 -0
- package/bin/minko.js +2 -2
- package/dist/agent/approvalTokens.js +224 -0
- package/dist/agent/branchLock.js +315 -0
- package/dist/agent/compact.js +182 -0
- package/dist/agent/context.js +183 -0
- package/dist/agent/cron.js +326 -0
- package/dist/agent/hooks.js +211 -0
- package/dist/agent/lanes.js +511 -0
- package/dist/agent/loop.js +337 -0
- package/dist/agent/memory.js +159 -0
- package/dist/agent/permissions.js +71 -0
- package/dist/agent/plan.js +118 -0
- package/dist/agent/prompt.js +120 -0
- package/dist/agent/subagent.js +67 -0
- package/dist/agent/tasks.js +449 -0
- package/dist/agent/transcript.js +62 -0
- package/dist/agent/workerStore.js +172 -0
- package/dist/agent/workerTypes.js +1 -0
- package/dist/agent/workers.js +503 -0
- package/dist/cli/app.js +261 -0
- package/dist/cli/argv.js +206 -0
- package/dist/cli/commandsCommand.js +12 -0
- package/dist/cli/configCommand.js +170 -0
- package/dist/cli/doctorCommand.js +112 -0
- package/dist/cli/exportCommand.js +85 -0
- package/dist/cli/forkCommand.js +24 -0
- package/dist/cli/linkCommand.js +272 -0
- package/dist/cli/loginCommand.js +69 -0
- package/dist/cli/mcpLoginCommand.js +68 -0
- package/dist/cli/pluginCommand.js +113 -0
- package/dist/cli/powerCommand.js +174 -0
- package/dist/cli/promptFooter.js +74 -0
- package/dist/cli/relayCommand.js +38 -0
- package/dist/cli/sandboxCommand.js +31 -0
- package/dist/cli/serveCommand.js +104 -0
- package/dist/cli/statusCommand.js +120 -0
- package/dist/cli/tui.js +1104 -0
- package/dist/commands/registry.js +99 -0
- package/dist/config/paths.js +81 -0
- package/dist/config/store.js +605 -0
- package/dist/index.js +199 -0
- package/dist/link/protocol.js +159 -0
- package/dist/link/relay.js +184 -0
- package/dist/link/server.js +566 -0
- package/dist/link/tunnel.js +90 -0
- package/dist/lsp/client.js +340 -0
- package/dist/lsp/tool.js +177 -0
- package/dist/mcp/client.js +469 -0
- package/dist/mcp/httpClient.js +191 -0
- package/dist/mcp/server.js +206 -0
- package/dist/mcp/tools.js +173 -0
- package/dist/oauth/device.js +111 -0
- package/dist/oauth/store.js +56 -0
- package/dist/plugins/loader.js +291 -0
- package/dist/plugins/market.js +98 -0
- package/dist/providers/anthropic.js +279 -0
- package/dist/providers/factory.js +109 -0
- package/dist/providers/openai.js +182 -0
- package/dist/providers/retry.js +95 -0
- package/dist/providers/sse.js +67 -0
- package/dist/providers/types.js +10 -0
- package/dist/rag/embeddings.js +171 -0
- package/dist/rag/index.js +373 -0
- package/dist/rag/server.js +167 -0
- package/dist/rag/tool.js +148 -0
- package/dist/sandbox/index.js +55 -0
- package/dist/sdk/server.js +150 -0
- package/dist/sdk/session.js +104 -0
- package/dist/session/liveness.js +148 -0
- package/dist/session/recorder.js +49 -0
- package/dist/session/store.js +171 -0
- package/dist/skills/index.js +428 -0
- package/dist/theme.js +30 -0
- package/dist/tools/fs.js +195 -0
- package/dist/tools/notebook.js +109 -0
- package/dist/tools/registry.js +32 -0
- package/dist/tools/search.js +130 -0
- package/dist/tools/shell.js +200 -0
- package/dist/tools/structured.js +83 -0
- package/dist/tools/types.js +1 -0
- package/dist/ui/ansi.js +135 -0
- package/dist/ui/banner.js +103 -0
- package/dist/ui/input.js +49 -0
- package/dist/ui/inputModel.js +113 -0
- package/dist/ui/keyboard.js +142 -0
- package/dist/ui/mascot.js +34 -0
- package/dist/ui/mascotData.js +96 -0
- package/dist/ui/md.js +183 -0
- package/dist/ui/screen.js +49 -0
- package/dist/ui/spinner.js +52 -0
- package/dist/ui/transcript.js +287 -0
- package/dist/util/clipboard.js +53 -0
- package/dist/util/errors.js +50 -0
- package/dist/util/version.js +12 -0
- package/dist/web/fetch.js +168 -0
- package/dist/web/search.js +97 -0
- package/package.json +45 -36
- package/src/agent/loop.ts +0 -159
- package/src/agent/prompt.ts +0 -45
- package/src/cli/app.ts +0 -396
- package/src/cli/argv.ts +0 -139
- package/src/cli/configCommand.ts +0 -175
- package/src/config/paths.ts +0 -20
- package/src/config/store.ts +0 -149
- package/src/index.ts +0 -120
- package/src/providers/anthropic.ts +0 -215
- package/src/providers/openai.ts +0 -158
- package/src/providers/sse.ts +0 -57
- package/src/providers/types.ts +0 -59
- package/src/session/store.ts +0 -125
- package/src/theme.ts +0 -41
- package/src/tools/fs.ts +0 -143
- package/src/tools/registry.ts +0 -19
- package/src/tools/shell.ts +0 -120
- package/src/tools/types.ts +0 -26
- package/src/ui/ansi.ts +0 -110
- package/src/ui/banner.ts +0 -39
- package/src/ui/input.ts +0 -54
- package/src/ui/md.ts +0 -160
- package/src/ui/spinner.ts +0 -58
- package/src/util/errors.ts +0 -48
package/dist/index.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { parseArgs, HELP_TEXT } from "./cli/argv.js";
|
|
3
|
+
import { runConfigCommand } from "./cli/configCommand.js";
|
|
4
|
+
import { runPowerCommand } from "./cli/powerCommand.js";
|
|
5
|
+
import { runPluginCommand } from "./cli/pluginCommand.js";
|
|
6
|
+
import { runForkCommand } from "./cli/forkCommand.js";
|
|
7
|
+
import { runLoginCommand } from "./cli/loginCommand.js";
|
|
8
|
+
import { runStatusCommand } from "./cli/statusCommand.js";
|
|
9
|
+
import { runDoctorCommand } from "./cli/doctorCommand.js";
|
|
10
|
+
import { runSandboxCommand } from "./cli/sandboxCommand.js";
|
|
11
|
+
import { runRagServer } from "./rag/server.js";
|
|
12
|
+
import { runMcpServer } from "./mcp/server.js";
|
|
13
|
+
import { runMcpLoginCommand } from "./cli/mcpLoginCommand.js";
|
|
14
|
+
import { runSdkServer } from "./sdk/server.js";
|
|
15
|
+
import { runCommandsCommand } from "./cli/commandsCommand.js";
|
|
16
|
+
import { runExportCommand } from "./cli/exportCommand.js";
|
|
17
|
+
import { runLinkCommand } from "./cli/linkCommand.js";
|
|
18
|
+
import { runRelayCommand } from "./cli/relayCommand.js";
|
|
19
|
+
import { runServeCommand } from "./cli/serveCommand.js";
|
|
20
|
+
import { runApp } from "./cli/app.js";
|
|
21
|
+
import { activeProvider, configProblems, loadConfig, saveConfig, } from "./config/store.js";
|
|
22
|
+
import { homeFallbackWarning } from "./config/paths.js";
|
|
23
|
+
import { isLive, listStatuses, statusLabel } from "./session/liveness.js";
|
|
24
|
+
import { readVersion } from "./util/version.js";
|
|
25
|
+
import { providerChain } from "./providers/factory.js";
|
|
26
|
+
import { SessionStore } from "./session/store.js";
|
|
27
|
+
import { Painter } from "./ui/ansi.js";
|
|
28
|
+
export async function main(argv) {
|
|
29
|
+
const painter = new Painter();
|
|
30
|
+
let parsed;
|
|
31
|
+
try {
|
|
32
|
+
parsed = parseArgs(argv);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
process.stderr.write(painter.error((error instanceof Error ? error.message : String(error)) + '\n'));
|
|
36
|
+
process.exitCode = 1;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
switch (parsed.command) {
|
|
40
|
+
case 'help':
|
|
41
|
+
process.stdout.write(HELP_TEXT + '\n');
|
|
42
|
+
return;
|
|
43
|
+
case 'version':
|
|
44
|
+
process.stdout.write('minko ' + readVersion() + '\n');
|
|
45
|
+
return;
|
|
46
|
+
case 'link':
|
|
47
|
+
process.exitCode = await runLinkCommand(parsed.port, parsed.host, parsed.relay, parsed.lanOnly);
|
|
48
|
+
return;
|
|
49
|
+
case 'relay':
|
|
50
|
+
process.exitCode = await runRelayCommand(parsed.port);
|
|
51
|
+
return;
|
|
52
|
+
case 'serve':
|
|
53
|
+
process.exitCode = await runServeCommand();
|
|
54
|
+
return;
|
|
55
|
+
case 'sessions':
|
|
56
|
+
printSessions(painter, parsed.json);
|
|
57
|
+
return;
|
|
58
|
+
case 'power':
|
|
59
|
+
process.exitCode = await runPowerCommand(parsed.mode);
|
|
60
|
+
return;
|
|
61
|
+
case 'plugin':
|
|
62
|
+
process.exitCode = await runPluginCommand(parsed.action, parsed.target);
|
|
63
|
+
return;
|
|
64
|
+
case 'fork':
|
|
65
|
+
process.exitCode = await runForkCommand(parsed.id, parsed.name);
|
|
66
|
+
return;
|
|
67
|
+
case 'login':
|
|
68
|
+
process.exitCode = await runLoginCommand();
|
|
69
|
+
return;
|
|
70
|
+
case 'status':
|
|
71
|
+
process.exitCode = await runStatusCommand(parsed.json);
|
|
72
|
+
return;
|
|
73
|
+
case 'doctor':
|
|
74
|
+
process.exitCode = await runDoctorCommand(parsed.json);
|
|
75
|
+
return;
|
|
76
|
+
case 'sandbox':
|
|
77
|
+
process.exitCode = await runSandboxCommand(parsed.json);
|
|
78
|
+
return;
|
|
79
|
+
case 'rag':
|
|
80
|
+
process.exitCode = await runRagServer({ port: parsed.port, host: parsed.host });
|
|
81
|
+
return;
|
|
82
|
+
case 'mcp-server':
|
|
83
|
+
process.exitCode = await runMcpServer();
|
|
84
|
+
return;
|
|
85
|
+
case 'mcp-login':
|
|
86
|
+
process.exitCode = await runMcpLoginCommand(parsed.server);
|
|
87
|
+
return;
|
|
88
|
+
case 'commands':
|
|
89
|
+
process.exitCode = await runCommandsCommand(parsed.json);
|
|
90
|
+
return;
|
|
91
|
+
case 'sdk': {
|
|
92
|
+
const sdkConfig = loadConfig();
|
|
93
|
+
const sdkProblems = configProblems(sdkConfig);
|
|
94
|
+
if (sdkProblems.length > 0) {
|
|
95
|
+
for (const problem of sdkProblems) {
|
|
96
|
+
process.stderr.write(painter.error('· ' + problem + '\n'));
|
|
97
|
+
}
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const sdkChain = providerChain(sdkConfig);
|
|
102
|
+
if (!sdkChain) {
|
|
103
|
+
process.exitCode = 1;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
process.exitCode = await runSdkServer({
|
|
107
|
+
config: sdkConfig,
|
|
108
|
+
getProvider: () => sdkChain.provider,
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
case 'export':
|
|
113
|
+
process.exitCode = await runExportCommand(parsed.id, parsed.format, parsed.out);
|
|
114
|
+
return;
|
|
115
|
+
case 'config': {
|
|
116
|
+
process.exitCode = await runConfigCommand(parsed);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
case 'run':
|
|
120
|
+
break;
|
|
121
|
+
default:
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const config = loadConfig();
|
|
125
|
+
const fallbackNote = homeFallbackWarning();
|
|
126
|
+
if (fallbackNote) {
|
|
127
|
+
process.stderr.write(painter.warn('· ' + fallbackNote) + '\n');
|
|
128
|
+
}
|
|
129
|
+
const problems = configProblems(config);
|
|
130
|
+
if (problems.length > 0) {
|
|
131
|
+
for (const problem of problems) {
|
|
132
|
+
process.stderr.write(painter.error('· ' + problem + '\n'));
|
|
133
|
+
}
|
|
134
|
+
process.exitCode = 1;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const chain = providerChain(config, parsed.model);
|
|
138
|
+
if (!chain) {
|
|
139
|
+
process.exitCode = 1;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const { provider, model } = chain;
|
|
143
|
+
process.exitCode = await runApp({
|
|
144
|
+
config,
|
|
145
|
+
provider,
|
|
146
|
+
providerName: chain.name,
|
|
147
|
+
model,
|
|
148
|
+
cwd: process.cwd(),
|
|
149
|
+
auto: parsed.auto,
|
|
150
|
+
print: parsed.print,
|
|
151
|
+
prompt: parsed.prompt,
|
|
152
|
+
continueSession: parsed.continue,
|
|
153
|
+
resumeId: parsed.resume,
|
|
154
|
+
version: readVersion(),
|
|
155
|
+
setModel: (next) => {
|
|
156
|
+
const fresh = loadConfig();
|
|
157
|
+
const entry = fresh.providers[fresh.active];
|
|
158
|
+
if (!entry) {
|
|
159
|
+
throw new Error('当前没有可用的模型配置');
|
|
160
|
+
}
|
|
161
|
+
const chain = providerChain(fresh, next);
|
|
162
|
+
if (!chain) {
|
|
163
|
+
throw new Error('当前没有可用的模型配置');
|
|
164
|
+
}
|
|
165
|
+
saveConfig({
|
|
166
|
+
...fresh,
|
|
167
|
+
providers: { ...fresh.providers, [fresh.active]: { ...entry, model: next } },
|
|
168
|
+
});
|
|
169
|
+
return chain.provider;
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
function printSessions(painter, json) {
|
|
174
|
+
const store = new SessionStore();
|
|
175
|
+
const metas = store.list();
|
|
176
|
+
const statuses = new Map(listStatuses(store.dir).map((status) => [status.sessionId, status]));
|
|
177
|
+
const rows = metas.map((meta) => {
|
|
178
|
+
const status = statuses.get(meta.id);
|
|
179
|
+
const live = isLive(status ?? null);
|
|
180
|
+
return {
|
|
181
|
+
...meta,
|
|
182
|
+
status: live ? status?.status ?? null : null,
|
|
183
|
+
statusLabel: statusLabel(live ? status?.status : null),
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
if (json) {
|
|
187
|
+
process.stdout.write(JSON.stringify(rows, null, 2) + '\n');
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (rows.length === 0) {
|
|
191
|
+
process.stdout.write(' ' + painter.muted('还没有保存的会话。') + '\n');
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
for (const meta of rows.slice(0, 30)) {
|
|
195
|
+
process.stdout.write(' ' + painter.primary('●') + ' ' + painter.soft(meta.id) + ' '
|
|
196
|
+
+ painter.muted(meta.title) + ' ' + painter.dimmed('[' + meta.statusLabel + ']') + '\n '
|
|
197
|
+
+ painter.dimmed(meta.cwd + ' · ' + meta.updatedAt.slice(0, 16).replace('T', ' ')) + '\n');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { minkoHome } from "../config/paths.js";
|
|
6
|
+
export function encodeLink(payload) {
|
|
7
|
+
const params = new URLSearchParams();
|
|
8
|
+
params.set('h', payload.host);
|
|
9
|
+
params.set('p', String(payload.port));
|
|
10
|
+
params.set('t', payload.token);
|
|
11
|
+
params.set('n', payload.name);
|
|
12
|
+
if (payload.relay && payload.relay.length > 0) {
|
|
13
|
+
try {
|
|
14
|
+
const url = new URL(payload.relay);
|
|
15
|
+
params.set('s', url.protocol === 'https:' ? '1' : '0');
|
|
16
|
+
params.set('r', url.host);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
params.set('s', '0');
|
|
20
|
+
params.set('r', payload.relay.replace(/^https?:\/\//, ''));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (payload.room && payload.room.length > 0) {
|
|
24
|
+
params.set('o', payload.room);
|
|
25
|
+
}
|
|
26
|
+
return 'minko://l?' + params.toString();
|
|
27
|
+
}
|
|
28
|
+
export function decodeLink(uri) {
|
|
29
|
+
const text = uri.trim();
|
|
30
|
+
if (text.startsWith('minko://link?d=')) {
|
|
31
|
+
return decodeLegacy(text);
|
|
32
|
+
}
|
|
33
|
+
if (!text.startsWith('minko://l?')) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const params = new URLSearchParams(text.slice('minko://l?'.length));
|
|
38
|
+
const host = params.get('h') ?? '';
|
|
39
|
+
const port = Number(params.get('p') ?? '0');
|
|
40
|
+
const token = params.get('t') ?? '';
|
|
41
|
+
if (host.length === 0 || !Number.isInteger(port) || port <= 0 || token.length === 0) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const relayHost = params.get('r');
|
|
45
|
+
const payload = {
|
|
46
|
+
v: 1,
|
|
47
|
+
host,
|
|
48
|
+
port,
|
|
49
|
+
token,
|
|
50
|
+
name: params.get('n') ?? host,
|
|
51
|
+
};
|
|
52
|
+
if (relayHost && relayHost.length > 0) {
|
|
53
|
+
payload.relay = (params.get('s') === '1' ? 'https://' : 'http://') + relayHost;
|
|
54
|
+
payload.room = params.get('o') ?? '';
|
|
55
|
+
}
|
|
56
|
+
return payload;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function decodeLegacy(uri) {
|
|
63
|
+
const match = /^minko:\/\/link\?d=([A-Za-z0-9_-]+)$/.exec(uri.trim());
|
|
64
|
+
if (!match) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(Buffer.from(match[1], 'base64url').toString('utf8'));
|
|
69
|
+
if (parsed.v !== 1 || typeof parsed.host !== 'string' || typeof parsed.port !== 'number'
|
|
70
|
+
|| typeof parsed.token !== 'string') {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
return parsed;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export function devicePath() {
|
|
80
|
+
return path.join(minkoHome(), 'device.json');
|
|
81
|
+
}
|
|
82
|
+
export function loadOrCreateDevice() {
|
|
83
|
+
const file = devicePath();
|
|
84
|
+
if (fs.existsSync(file)) {
|
|
85
|
+
try {
|
|
86
|
+
const saved = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
87
|
+
if (saved.id && saved.privateKey && saved.publicKey) {
|
|
88
|
+
return saved;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// regenerate below
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
|
|
96
|
+
const identity = {
|
|
97
|
+
id: crypto.randomUUID(),
|
|
98
|
+
name: os.hostname() || 'minko-pc',
|
|
99
|
+
publicKey: publicKey.export({ type: 'spki', format: 'der' }).toString('base64'),
|
|
100
|
+
privateKey: privateKey.export({ type: 'pkcs8', format: 'pem' }).toString(),
|
|
101
|
+
phones: [],
|
|
102
|
+
};
|
|
103
|
+
saveDevice(identity);
|
|
104
|
+
return identity;
|
|
105
|
+
}
|
|
106
|
+
export function saveDevice(identity) {
|
|
107
|
+
const file = devicePath();
|
|
108
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
109
|
+
fs.writeFileSync(file, JSON.stringify(identity, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
|
|
110
|
+
}
|
|
111
|
+
export function recordPhone(identity, name) {
|
|
112
|
+
const phones = identity.phones ?? [];
|
|
113
|
+
const clean = name.trim().slice(0, 24) || '手机';
|
|
114
|
+
if (phones.some((phone) => phone.name === clean)) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
phones.push({ name: clean, at: new Date().toISOString() });
|
|
118
|
+
identity.phones = phones;
|
|
119
|
+
saveDevice(identity);
|
|
120
|
+
}
|
|
121
|
+
export function pidFilePath() {
|
|
122
|
+
return path.join(minkoHome(), 'serve.pid');
|
|
123
|
+
}
|
|
124
|
+
export function newToken() {
|
|
125
|
+
return crypto.randomBytes(12).toString('hex');
|
|
126
|
+
}
|
|
127
|
+
/** Prefers a real LAN address; skips VPN/tunnel adapters like 198.18/15. */
|
|
128
|
+
export function lanAddress() {
|
|
129
|
+
const interfaces = os.networkInterfaces();
|
|
130
|
+
const candidates = [];
|
|
131
|
+
for (const addresses of Object.values(interfaces)) {
|
|
132
|
+
for (const address of addresses ?? []) {
|
|
133
|
+
if (address.family !== 'IPv4' || address.internal) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (address.address.startsWith('198.18.') || address.address.startsWith('198.19.')) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (address.address.startsWith('169.254.')) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
candidates.push(address.address);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const score = (ip) => {
|
|
146
|
+
if (ip.startsWith('192.168.')) {
|
|
147
|
+
return 3;
|
|
148
|
+
}
|
|
149
|
+
if (ip.startsWith('10.')) {
|
|
150
|
+
return 2;
|
|
151
|
+
}
|
|
152
|
+
if (/^172\.(1[6-9]|2\d|3[01])\./.test(ip)) {
|
|
153
|
+
return 1;
|
|
154
|
+
}
|
|
155
|
+
return 0;
|
|
156
|
+
};
|
|
157
|
+
candidates.sort((a, b) => score(b) - score(a));
|
|
158
|
+
return candidates.length > 0 ? candidates[0] : '127.0.0.1';
|
|
159
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
const CALL_TIMEOUT_MS = 120000;
|
|
4
|
+
const MAX_BODY = 1024 * 1024;
|
|
5
|
+
/**
|
|
6
|
+
* Public relay for `minko -link --relay`. The PC keeps an outbound SSE tunnel,
|
|
7
|
+
* the phone posts calls here, and the relay forwards them to the PC and back.
|
|
8
|
+
*/
|
|
9
|
+
export class RelayServer {
|
|
10
|
+
server = null;
|
|
11
|
+
rooms = new Map();
|
|
12
|
+
async start(host, port) {
|
|
13
|
+
this.server = http.createServer((request, response) => {
|
|
14
|
+
void this.handle(request, response).catch(() => {
|
|
15
|
+
writeText(response, 500, 'relay error');
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
await new Promise((resolve, reject) => {
|
|
19
|
+
this.server?.once('error', reject);
|
|
20
|
+
this.server?.listen(port, host, () => resolve());
|
|
21
|
+
});
|
|
22
|
+
const address = this.server.address();
|
|
23
|
+
return typeof address === 'object' && address ? address.port : port;
|
|
24
|
+
}
|
|
25
|
+
async stop() {
|
|
26
|
+
for (const room of this.rooms.values()) {
|
|
27
|
+
room.agent?.end();
|
|
28
|
+
}
|
|
29
|
+
this.rooms.clear();
|
|
30
|
+
await new Promise((resolve) => {
|
|
31
|
+
if (!this.server) {
|
|
32
|
+
resolve();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const server = this.server;
|
|
36
|
+
this.server = null;
|
|
37
|
+
server.close(() => resolve());
|
|
38
|
+
server.closeAllConnections?.();
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
async handle(request, response) {
|
|
42
|
+
const url = new URL(request.url ?? '/', 'http://localhost');
|
|
43
|
+
const path = url.pathname;
|
|
44
|
+
if (path === '/health') {
|
|
45
|
+
writeJson(response, 200, { ok: true, rooms: this.rooms.size });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const tunnel = /^\/tunnel\/([A-Za-z0-9_-]+)$/.exec(path);
|
|
49
|
+
if (tunnel && request.method === 'GET') {
|
|
50
|
+
this.connect(tunnel[1], request, response);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const reply = /^\/tunnel\/([A-Za-z0-9_-]+)\/response$/.exec(path);
|
|
54
|
+
if (reply && request.method === 'POST') {
|
|
55
|
+
await this.deliver(reply[1], request, response);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const call = /^\/relay\/([A-Za-z0-9_-]+)\/call$/.exec(path);
|
|
59
|
+
if (call && request.method === 'POST') {
|
|
60
|
+
await this.call(call[1], request, response);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const status = /^\/relay\/([A-Za-z0-9_-]+)\/health$/.exec(path);
|
|
64
|
+
if (status) {
|
|
65
|
+
const room = this.rooms.get(status[1]);
|
|
66
|
+
writeJson(response, 200, { online: Boolean(room && room.agent) });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
writeJson(response, 404, { error: 'not found' });
|
|
70
|
+
}
|
|
71
|
+
connect(roomId, request, response) {
|
|
72
|
+
const token = bearer(request);
|
|
73
|
+
let room = this.rooms.get(roomId);
|
|
74
|
+
if (!room) {
|
|
75
|
+
room = { token, agent: null, pending: new Map() };
|
|
76
|
+
this.rooms.set(roomId, room);
|
|
77
|
+
}
|
|
78
|
+
else if (room.token !== token) {
|
|
79
|
+
writeJson(response, 401, { error: 'unauthorized' });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
room.agent?.end();
|
|
83
|
+
room.agent = response;
|
|
84
|
+
response.writeHead(200, {
|
|
85
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
86
|
+
'cache-control': 'no-cache',
|
|
87
|
+
connection: 'keep-alive',
|
|
88
|
+
});
|
|
89
|
+
response.write('event: ready\ndata: {}\n\n');
|
|
90
|
+
request.on('close', () => {
|
|
91
|
+
if (room && room.agent === response) {
|
|
92
|
+
room.agent = null;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
async deliver(roomId, request, response) {
|
|
97
|
+
const room = this.rooms.get(roomId);
|
|
98
|
+
if (!room || room.token !== bearer(request)) {
|
|
99
|
+
writeJson(response, 401, { error: 'unauthorized' });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const payload = await readJson(request);
|
|
103
|
+
const id = typeof payload.id === 'string' ? payload.id : '';
|
|
104
|
+
const pending = room.pending.get(id);
|
|
105
|
+
if (!pending) {
|
|
106
|
+
writeJson(response, 404, { error: 'unknown call' });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
room.pending.delete(id);
|
|
110
|
+
pending.resolve({
|
|
111
|
+
status: typeof payload.status === 'number' ? payload.status : 200,
|
|
112
|
+
body: typeof payload.body === 'string' ? payload.body : '',
|
|
113
|
+
});
|
|
114
|
+
writeJson(response, 200, { ok: true });
|
|
115
|
+
}
|
|
116
|
+
async call(roomId, request, response) {
|
|
117
|
+
const room = this.rooms.get(roomId);
|
|
118
|
+
if (!room || room.token !== bearer(request)) {
|
|
119
|
+
writeJson(response, 401, { error: 'unauthorized' });
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (!room.agent) {
|
|
123
|
+
writeJson(response, 503, { error: '电脑不在线' });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const payload = await readJson(request);
|
|
127
|
+
const id = crypto.randomBytes(8).toString('hex');
|
|
128
|
+
const method = typeof payload.method === 'string' ? payload.method : 'GET';
|
|
129
|
+
const path = typeof payload.path === 'string' ? payload.path : '/sessions';
|
|
130
|
+
const body = typeof payload.body === 'string' ? payload.body : '';
|
|
131
|
+
const result = await new Promise((resolve) => {
|
|
132
|
+
const timer = setTimeout(() => {
|
|
133
|
+
room.pending.delete(id);
|
|
134
|
+
resolve({ status: 504, body: '{"error":"电脑响应超时"}' });
|
|
135
|
+
}, CALL_TIMEOUT_MS);
|
|
136
|
+
room.pending.set(id, {
|
|
137
|
+
resolve: (value) => {
|
|
138
|
+
clearTimeout(timer);
|
|
139
|
+
resolve(value);
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
room.agent?.write('event: request\ndata: '
|
|
143
|
+
+ JSON.stringify({ id, method, path, body }) + '\n\n');
|
|
144
|
+
});
|
|
145
|
+
response.writeHead(result.status, { 'content-type': 'application/json; charset=utf-8' });
|
|
146
|
+
response.end(result.body);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function bearer(request) {
|
|
150
|
+
return (request.headers.authorization ?? '').replace(/^Bearer\s+/i, '');
|
|
151
|
+
}
|
|
152
|
+
function writeJson(response, status, body) {
|
|
153
|
+
const payload = JSON.stringify(body);
|
|
154
|
+
response.writeHead(status, {
|
|
155
|
+
'content-type': 'application/json; charset=utf-8',
|
|
156
|
+
'content-length': Buffer.byteLength(payload),
|
|
157
|
+
});
|
|
158
|
+
response.end(payload);
|
|
159
|
+
}
|
|
160
|
+
function writeText(response, status, text) {
|
|
161
|
+
response.writeHead(status, { 'content-type': 'text/plain; charset=utf-8' });
|
|
162
|
+
response.end(text);
|
|
163
|
+
}
|
|
164
|
+
async function readJson(request) {
|
|
165
|
+
let size = 0;
|
|
166
|
+
const chunks = [];
|
|
167
|
+
for await (const chunk of request) {
|
|
168
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
169
|
+
size += buffer.length;
|
|
170
|
+
if (size > MAX_BODY) {
|
|
171
|
+
throw new Error('请求体过大');
|
|
172
|
+
}
|
|
173
|
+
chunks.push(buffer);
|
|
174
|
+
}
|
|
175
|
+
if (chunks.length === 0) {
|
|
176
|
+
return {};
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return {};
|
|
183
|
+
}
|
|
184
|
+
}
|