@coralai/sps-cli 0.68.0 → 0.69.0
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/CHANGELOG.md +23 -0
- package/assembly/base.yml +23 -0
- package/dist/console/index.d.ts.map +1 -1
- package/dist/console/index.js +27 -0
- package/dist/console/index.js.map +1 -1
- package/dist/console/routes/plugins.d.ts.map +1 -1
- package/dist/console/routes/plugins.js +21 -2
- package/dist/console/routes/plugins.js.map +1 -1
- package/dist/console/serviceAuth.d.ts +6 -2
- package/dist/console/serviceAuth.d.ts.map +1 -1
- package/dist/console/serviceAuth.js +10 -4
- package/dist/console/serviceAuth.js.map +1 -1
- package/dist/console-assets/assets/{ShowcasePage-DyKTVaXk.js → ShowcasePage-BaaX3Cpt.js} +1 -1
- package/dist/console-assets/assets/{index-D-8ikIK_.css → index-BDmCjVjN.css} +1 -1
- package/dist/console-assets/assets/{index-BN0r9VlY.js → index-BgAaYxbH.js} +3 -3
- package/dist/console-assets/index.html +2 -2
- package/dist/plugin/manager.d.ts +6 -0
- package/dist/plugin/manager.d.ts.map +1 -1
- package/dist/plugin/manager.js +164 -26
- package/dist/plugin/manager.js.map +1 -1
- package/dist/plugin/patchFile.d.ts +27 -0
- package/dist/plugin/patchFile.d.ts.map +1 -0
- package/dist/plugin/patchFile.js +71 -0
- package/dist/plugin/patchFile.js.map +1 -0
- package/dist/seams/attachments/index.d.ts +28 -0
- package/dist/seams/attachments/index.d.ts.map +1 -0
- package/dist/seams/attachments/index.js +72 -0
- package/dist/seams/attachments/index.js.map +1 -0
- package/dist/seams/compat-dsh/credentials.d.ts +28 -0
- package/dist/seams/compat-dsh/credentials.d.ts.map +1 -0
- package/dist/seams/compat-dsh/credentials.js +33 -0
- package/dist/seams/compat-dsh/credentials.js.map +1 -0
- package/dist/seams/compat-dsh/index.d.ts +26 -0
- package/dist/seams/compat-dsh/index.d.ts.map +1 -0
- package/dist/seams/compat-dsh/index.js +21 -0
- package/dist/seams/compat-dsh/index.js.map +1 -0
- package/dist/seams/compat-dsh/misc.d.ts +31 -0
- package/dist/seams/compat-dsh/misc.d.ts.map +1 -0
- package/dist/seams/compat-dsh/misc.js +41 -0
- package/dist/seams/compat-dsh/misc.js.map +1 -0
- package/dist/seams/compat-dsh/tools.d.ts +71 -0
- package/dist/seams/compat-dsh/tools.d.ts.map +1 -0
- package/dist/seams/compat-dsh/tools.js +92 -0
- package/dist/seams/compat-dsh/tools.js.map +1 -0
- package/dist/seams/compat-dsh/webserver.d.ts +43 -0
- package/dist/seams/compat-dsh/webserver.d.ts.map +1 -0
- package/dist/seams/compat-dsh/webserver.js +50 -0
- package/dist/seams/compat-dsh/webserver.js.map +1 -0
- package/dist/seams/credentials/index.d.ts +52 -0
- package/dist/seams/credentials/index.d.ts.map +1 -0
- package/dist/seams/credentials/index.js +33 -0
- package/dist/seams/credentials/index.js.map +1 -0
- package/dist/seams/credentials-local/index.d.ts +12 -0
- package/dist/seams/credentials-local/index.d.ts.map +1 -0
- package/dist/seams/credentials-local/index.js +93 -0
- package/dist/seams/credentials-local/index.js.map +1 -0
- package/dist/seams/subprocess/index.d.ts +34 -0
- package/dist/seams/subprocess/index.d.ts.map +1 -0
- package/dist/seams/subprocess/index.js +77 -0
- package/dist/seams/subprocess/index.js.map +1 -0
- package/dist/seams/webserver/index.d.ts +92 -0
- package/dist/seams/webserver/index.d.ts.map +1 -0
- package/dist/seams/webserver/index.js +100 -0
- package/dist/seams/webserver/index.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type Context, Service } from '@deepseek-ai/cordis';
|
|
2
|
+
export declare const SUBPROCESS = "sps.subprocess";
|
|
3
|
+
export interface SpawnSpec {
|
|
4
|
+
argv: readonly string[];
|
|
5
|
+
cwd: string;
|
|
6
|
+
env?: NodeJS.ProcessEnv;
|
|
7
|
+
signal?: AbortSignal;
|
|
8
|
+
/** 收到取消后,等它自己退出多久才 SIGKILL。默认 5000。 */
|
|
9
|
+
graceMs?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface SpawnResult {
|
|
12
|
+
code: number | null;
|
|
13
|
+
stdout: string;
|
|
14
|
+
stderr: string;
|
|
15
|
+
}
|
|
16
|
+
export default class SubprocessSeam extends Service {
|
|
17
|
+
constructor(ctx: Context);
|
|
18
|
+
/** 把命令名解析成可执行路径(`which`)。找不到返回 `undefined`。 */
|
|
19
|
+
resolveExecutable(cmd: string): Promise<string | undefined>;
|
|
20
|
+
/**
|
|
21
|
+
* 跑一个命令,收全输出。
|
|
22
|
+
*
|
|
23
|
+
* 🔴 **取消是两段的**:先 SIGTERM,`graceMs` 之后才 SIGKILL。
|
|
24
|
+
* 直接 SIGKILL 会让子进程来不及收拾(临时文件、锁),
|
|
25
|
+
* 而那些残留没人记得是谁留下的。
|
|
26
|
+
*/
|
|
27
|
+
spawn(spec: SpawnSpec): Promise<SpawnResult>;
|
|
28
|
+
}
|
|
29
|
+
declare module '@deepseek-ai/cordis' {
|
|
30
|
+
interface Context {
|
|
31
|
+
'sps.subprocess': SubprocessSeam;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/seams/subprocess/index.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,KAAK,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAE5D,eAAO,MAAM,UAAU,mBAAmB,CAAC;AAE3C,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,uCAAuC;IACvC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,CAAC,OAAO,OAAO,cAAe,SAAQ,OAAO;gBACrC,GAAG,EAAE,OAAO;IAIxB,+CAA+C;IACzC,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAQjE;;;;;;OAMG;IACH,KAAK,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC;CAmC7C;AAED,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf,gBAAgB,EAAE,cAAc,CAAC;KAClC;CACF"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module seams/subprocess
|
|
3
|
+
* @description **子进程 seam** —— 插件起外部命令的口子。
|
|
4
|
+
*
|
|
5
|
+
* @layer seam
|
|
6
|
+
*
|
|
7
|
+
* 🔴 **不迁移我们自己的 47 个文件。** 它们直用 `node:child_process`,
|
|
8
|
+
* 照旧能跑;这个 seam 是给**插件**用的门。
|
|
9
|
+
* "提供服务" ≠ "把宿主代码搬过来" —— 混为一谈会把一个低成本的口子
|
|
10
|
+
* 估成一次全仓重构。
|
|
11
|
+
*
|
|
12
|
+
* 形状按 dsh(`ctx.subprocess`):`resolveExecutable / spawn`。
|
|
13
|
+
* ⚠️ `spawnTerminal`(分配 PTY)**不做** —— 我们没有终端复用那套,
|
|
14
|
+
* 给一个假的比没有更坏。
|
|
15
|
+
*/
|
|
16
|
+
import { execFile, spawn as nodeSpawn } from 'node:child_process';
|
|
17
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
18
|
+
export const SUBPROCESS = 'sps.subprocess';
|
|
19
|
+
export default class SubprocessSeam extends Service {
|
|
20
|
+
constructor(ctx) {
|
|
21
|
+
super(ctx, SUBPROCESS);
|
|
22
|
+
}
|
|
23
|
+
/** 把命令名解析成可执行路径(`which`)。找不到返回 `undefined`。 */
|
|
24
|
+
async resolveExecutable(cmd) {
|
|
25
|
+
return await new Promise((res) => {
|
|
26
|
+
execFile('which', [cmd], (err, stdout) => {
|
|
27
|
+
res(err ? undefined : stdout.trim() || undefined);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 跑一个命令,收全输出。
|
|
33
|
+
*
|
|
34
|
+
* 🔴 **取消是两段的**:先 SIGTERM,`graceMs` 之后才 SIGKILL。
|
|
35
|
+
* 直接 SIGKILL 会让子进程来不及收拾(临时文件、锁),
|
|
36
|
+
* 而那些残留没人记得是谁留下的。
|
|
37
|
+
*/
|
|
38
|
+
spawn(spec) {
|
|
39
|
+
const [cmd, ...args] = spec.argv;
|
|
40
|
+
if (!cmd)
|
|
41
|
+
return Promise.reject(new Error('spawn: argv 为空'));
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
const child = nodeSpawn(cmd, args, {
|
|
44
|
+
cwd: spec.cwd,
|
|
45
|
+
...(spec.env ? { env: spec.env } : {}),
|
|
46
|
+
});
|
|
47
|
+
let stdout = '';
|
|
48
|
+
let stderr = '';
|
|
49
|
+
let killer;
|
|
50
|
+
child.stdout?.on('data', (d) => {
|
|
51
|
+
stdout += String(d);
|
|
52
|
+
});
|
|
53
|
+
child.stderr?.on('data', (d) => {
|
|
54
|
+
stderr += String(d);
|
|
55
|
+
});
|
|
56
|
+
const onAbort = () => {
|
|
57
|
+
child.kill('SIGTERM');
|
|
58
|
+
killer = setTimeout(() => child.kill('SIGKILL'), spec.graceMs ?? 5000);
|
|
59
|
+
// 别让这个定时器把进程钉住 —— 命令已经在退了
|
|
60
|
+
killer.unref?.();
|
|
61
|
+
};
|
|
62
|
+
spec.signal?.addEventListener('abort', onAbort, { once: true });
|
|
63
|
+
child.on('error', (e) => {
|
|
64
|
+
if (killer)
|
|
65
|
+
clearTimeout(killer);
|
|
66
|
+
reject(e);
|
|
67
|
+
});
|
|
68
|
+
child.on('close', (code) => {
|
|
69
|
+
if (killer)
|
|
70
|
+
clearTimeout(killer);
|
|
71
|
+
spec.signal?.removeEventListener('abort', onAbort);
|
|
72
|
+
resolve({ code, stdout, stderr });
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/seams/subprocess/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module seams/webserver
|
|
3
|
+
* @description **HTTP 路由 seam** —— 让插件往 console 的 HTTP 面注册自己的路由。
|
|
4
|
+
*
|
|
5
|
+
* @layer seam
|
|
6
|
+
*
|
|
7
|
+
* 类型:**甲 · 工厂型**(进程里一个)。dsh 抽样里第二高频(22/77)。
|
|
8
|
+
*
|
|
9
|
+
* ⚠️ **方向别搞混**:插件自己 `fetch` 外部服务(出方向)不需要任何服务,
|
|
10
|
+
* 也拦不住 —— 插件本来就是进程内受信任代码。这里管的是**入方向**:
|
|
11
|
+
* 插件在**我们 console 的鉴权面**上开一条路由,那条路由天然带 `sps_token` cookie。
|
|
12
|
+
*
|
|
13
|
+
* 🔴 **护栏形状(Eddy 2026-08-21 拍板)——防意外,不防故意:**
|
|
14
|
+
* ```
|
|
15
|
+
* 默认 路由自动落 /plugins/<插件id>/* 零配置零摩擦
|
|
16
|
+
* 接管宿主 显式声明 takeover: true 允许(sps-cli 是用户自己的工具)
|
|
17
|
+
* 可见 listTakeovers() 供插件页展示 ← 关键在这一格
|
|
18
|
+
* ```
|
|
19
|
+
* 真正的失败模式不是恶意,是**遗忘**:半年后某个接口行为不对,
|
|
20
|
+
* 没人记得有插件接管了它,排查会一路查进宿主代码 —— 而宿主代码是对的。
|
|
21
|
+
* **能接管 ≠ 接管了却没人知道。**
|
|
22
|
+
*
|
|
23
|
+
* 🔴 **为什么是"可变路由表 + 一条 catch-all",而不是往 Hono 上加路由:**
|
|
24
|
+
* Hono 注册后**删不掉**。而插件要能热卸载(停用/卸载当场下线)——
|
|
25
|
+
* 往 Hono 上加的话,停用之后那条路由还在,只是处理函数变成了幽灵。
|
|
26
|
+
*/
|
|
27
|
+
import { type Context, Service } from '@deepseek-ai/cordis';
|
|
28
|
+
export declare const WEBSERVER = "sps.webserver";
|
|
29
|
+
export type RouteKind = 'exact' | 'prefix';
|
|
30
|
+
export interface WebRoute {
|
|
31
|
+
kind: RouteKind;
|
|
32
|
+
/** 相对路径。默认会被前缀成 `/plugins/<owner>/<path>`;`takeover` 时按原样。 */
|
|
33
|
+
path: string;
|
|
34
|
+
/**
|
|
35
|
+
* 原生 Node 处理函数 —— **与 dsh 的 `WebRoute.handler` 同形**,
|
|
36
|
+
* 这样 dsh 插件不用改。
|
|
37
|
+
*/
|
|
38
|
+
handler: (req: unknown, res: unknown) => void | Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* 接管宿主已有路径。**允许,但必须显式写出来**,而且会出现在插件页上。
|
|
41
|
+
*/
|
|
42
|
+
takeover?: boolean;
|
|
43
|
+
}
|
|
44
|
+
interface Entry extends WebRoute {
|
|
45
|
+
owner: string;
|
|
46
|
+
/** 真正生效的绝对路径(前缀过的,或 takeover 的原样)。 */
|
|
47
|
+
full: string;
|
|
48
|
+
}
|
|
49
|
+
export declare class RouteConflictError extends Error {
|
|
50
|
+
}
|
|
51
|
+
/** 插件路由的默认前缀。 */
|
|
52
|
+
export declare function pluginPrefix(owner: string): string;
|
|
53
|
+
export default class WebServerSeam extends Service {
|
|
54
|
+
/** full path → entry。**可变表**,热卸载靠删这里的项。 */
|
|
55
|
+
private readonly routes;
|
|
56
|
+
constructor(ctx: Context);
|
|
57
|
+
/**
|
|
58
|
+
* 注册一条路由。`owner` = 插件 id,决定默认前缀,也是"谁接管了什么"的归属。
|
|
59
|
+
*
|
|
60
|
+
* 🔴 **重复路径抛冲突,不是后注册的静默赢** ——
|
|
61
|
+
* 静默赢会让两个插件里"没生效的那个"完全无法解释。
|
|
62
|
+
*/
|
|
63
|
+
register(owner: string, route: WebRoute): () => void;
|
|
64
|
+
/**
|
|
65
|
+
* 找一条能处理该路径的路由。exact 优先于 prefix;prefix 里**最长的赢**
|
|
66
|
+
* (否则 `/a` 会抢走 `/a/b` 的请求,而后者显然更具体)。
|
|
67
|
+
*/
|
|
68
|
+
match(path: string): Entry | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* 谁接管了哪些宿主路径 —— **插件页要摆出来的就是它**。
|
|
71
|
+
* 这是整套护栏里最关键的一格:能接管不可怕,接管了没人知道才可怕。
|
|
72
|
+
*/
|
|
73
|
+
listTakeovers(): {
|
|
74
|
+
owner: string;
|
|
75
|
+
path: string;
|
|
76
|
+
kind: RouteKind;
|
|
77
|
+
}[];
|
|
78
|
+
/** 全部路由(诊断用)。 */
|
|
79
|
+
list(): {
|
|
80
|
+
owner: string;
|
|
81
|
+
path: string;
|
|
82
|
+
kind: RouteKind;
|
|
83
|
+
takeover: boolean;
|
|
84
|
+
}[];
|
|
85
|
+
}
|
|
86
|
+
declare module '@deepseek-ai/cordis' {
|
|
87
|
+
interface Context {
|
|
88
|
+
'sps.webserver': WebServerSeam;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export {};
|
|
92
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/seams/webserver/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAAE,KAAK,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAE5D,eAAO,MAAM,SAAS,kBAAkB,CAAC;AAEzC,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE3C,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,SAAS,CAAC;IAChB,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,UAAU,KAAM,SAAQ,QAAQ;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;CACd;AAED,qBAAa,kBAAmB,SAAQ,KAAK;CAAG;AAEhD,iBAAiB;AACjB,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,MAAM,CAAC,OAAO,OAAO,aAAc,SAAQ,OAAO;IAChD,2CAA2C;IAC3C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4B;gBAEvC,GAAG,EAAE,OAAO;IAIxB;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,MAAM,IAAI;IAiBpD;;;OAGG;IACH,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,GAAG,SAAS;IAatC;;;OAGG;IACH,aAAa,IAAI;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,SAAS,CAAA;KAAE,EAAE;IAMnE,iBAAiB;IACjB,IAAI,IAAI;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,SAAS,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAA;KAAE,EAAE;CAQ9E;AAED,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf,eAAe,EAAE,aAAa,CAAC;KAChC;CACF"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module seams/webserver
|
|
3
|
+
* @description **HTTP 路由 seam** —— 让插件往 console 的 HTTP 面注册自己的路由。
|
|
4
|
+
*
|
|
5
|
+
* @layer seam
|
|
6
|
+
*
|
|
7
|
+
* 类型:**甲 · 工厂型**(进程里一个)。dsh 抽样里第二高频(22/77)。
|
|
8
|
+
*
|
|
9
|
+
* ⚠️ **方向别搞混**:插件自己 `fetch` 外部服务(出方向)不需要任何服务,
|
|
10
|
+
* 也拦不住 —— 插件本来就是进程内受信任代码。这里管的是**入方向**:
|
|
11
|
+
* 插件在**我们 console 的鉴权面**上开一条路由,那条路由天然带 `sps_token` cookie。
|
|
12
|
+
*
|
|
13
|
+
* 🔴 **护栏形状(Eddy 2026-08-21 拍板)——防意外,不防故意:**
|
|
14
|
+
* ```
|
|
15
|
+
* 默认 路由自动落 /plugins/<插件id>/* 零配置零摩擦
|
|
16
|
+
* 接管宿主 显式声明 takeover: true 允许(sps-cli 是用户自己的工具)
|
|
17
|
+
* 可见 listTakeovers() 供插件页展示 ← 关键在这一格
|
|
18
|
+
* ```
|
|
19
|
+
* 真正的失败模式不是恶意,是**遗忘**:半年后某个接口行为不对,
|
|
20
|
+
* 没人记得有插件接管了它,排查会一路查进宿主代码 —— 而宿主代码是对的。
|
|
21
|
+
* **能接管 ≠ 接管了却没人知道。**
|
|
22
|
+
*
|
|
23
|
+
* 🔴 **为什么是"可变路由表 + 一条 catch-all",而不是往 Hono 上加路由:**
|
|
24
|
+
* Hono 注册后**删不掉**。而插件要能热卸载(停用/卸载当场下线)——
|
|
25
|
+
* 往 Hono 上加的话,停用之后那条路由还在,只是处理函数变成了幽灵。
|
|
26
|
+
*/
|
|
27
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
28
|
+
export const WEBSERVER = 'sps.webserver';
|
|
29
|
+
export class RouteConflictError extends Error {
|
|
30
|
+
}
|
|
31
|
+
/** 插件路由的默认前缀。 */
|
|
32
|
+
export function pluginPrefix(owner) {
|
|
33
|
+
return `/plugins/${owner}`;
|
|
34
|
+
}
|
|
35
|
+
export default class WebServerSeam extends Service {
|
|
36
|
+
/** full path → entry。**可变表**,热卸载靠删这里的项。 */
|
|
37
|
+
routes = new Map();
|
|
38
|
+
constructor(ctx) {
|
|
39
|
+
super(ctx, WEBSERVER);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 注册一条路由。`owner` = 插件 id,决定默认前缀,也是"谁接管了什么"的归属。
|
|
43
|
+
*
|
|
44
|
+
* 🔴 **重复路径抛冲突,不是后注册的静默赢** ——
|
|
45
|
+
* 静默赢会让两个插件里"没生效的那个"完全无法解释。
|
|
46
|
+
*/
|
|
47
|
+
register(owner, route) {
|
|
48
|
+
const rel = route.path.startsWith('/') ? route.path : `/${route.path}`;
|
|
49
|
+
const full = route.takeover ? rel : `${pluginPrefix(owner)}${rel}`;
|
|
50
|
+
const key = `${route.kind}:${full}`;
|
|
51
|
+
const existing = this.routes.get(key);
|
|
52
|
+
if (existing) {
|
|
53
|
+
throw new RouteConflictError(`路由 ${full} 已被插件 ${existing.owner} 占用(${route.kind});同一路径不能注册两次`);
|
|
54
|
+
}
|
|
55
|
+
this.routes.set(key, { ...route, owner, full });
|
|
56
|
+
return () => {
|
|
57
|
+
const cur = this.routes.get(key);
|
|
58
|
+
if (cur && cur.owner === owner)
|
|
59
|
+
this.routes.delete(key);
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 找一条能处理该路径的路由。exact 优先于 prefix;prefix 里**最长的赢**
|
|
64
|
+
* (否则 `/a` 会抢走 `/a/b` 的请求,而后者显然更具体)。
|
|
65
|
+
*/
|
|
66
|
+
match(path) {
|
|
67
|
+
const exact = this.routes.get(`exact:${path}`);
|
|
68
|
+
if (exact)
|
|
69
|
+
return exact;
|
|
70
|
+
let best;
|
|
71
|
+
for (const e of this.routes.values()) {
|
|
72
|
+
if (e.kind !== 'prefix')
|
|
73
|
+
continue;
|
|
74
|
+
if (path === e.full || path.startsWith(`${e.full}/`)) {
|
|
75
|
+
if (!best || e.full.length > best.full.length)
|
|
76
|
+
best = e;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return best;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* 谁接管了哪些宿主路径 —— **插件页要摆出来的就是它**。
|
|
83
|
+
* 这是整套护栏里最关键的一格:能接管不可怕,接管了没人知道才可怕。
|
|
84
|
+
*/
|
|
85
|
+
listTakeovers() {
|
|
86
|
+
return [...this.routes.values()]
|
|
87
|
+
.filter((e) => e.takeover)
|
|
88
|
+
.map((e) => ({ owner: e.owner, path: e.full, kind: e.kind }));
|
|
89
|
+
}
|
|
90
|
+
/** 全部路由(诊断用)。 */
|
|
91
|
+
list() {
|
|
92
|
+
return [...this.routes.values()].map((e) => ({
|
|
93
|
+
owner: e.owner,
|
|
94
|
+
path: e.full,
|
|
95
|
+
kind: e.kind,
|
|
96
|
+
takeover: e.takeover === true,
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/seams/webserver/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAAgB,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAE5D,MAAM,CAAC,MAAM,SAAS,GAAG,eAAe,CAAC;AAyBzC,MAAM,OAAO,kBAAmB,SAAQ,KAAK;CAAG;AAEhD,iBAAiB;AACjB,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,OAAO,YAAY,KAAK,EAAE,CAAC;AAC7B,CAAC;AAED,MAAM,CAAC,OAAO,OAAO,aAAc,SAAQ,OAAO;IAChD,2CAA2C;IAC1B,MAAM,GAAG,IAAI,GAAG,EAAiB,CAAC;IAEnD,YAAY,GAAY;QACtB,KAAK,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;IACxB,CAAC;IAED;;;;;OAKG;IACH,QAAQ,CAAC,KAAa,EAAE,KAAe;QACrC,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;QACvE,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,KAAK,CAAC,GAAG,GAAG,EAAE,CAAC;QACnE,MAAM,GAAG,GAAG,GAAG,KAAK,CAAC,IAAI,IAAI,IAAI,EAAE,CAAC;QACpC,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACtC,IAAI,QAAQ,EAAE,CAAC;YACb,MAAM,IAAI,kBAAkB,CAC1B,MAAM,IAAI,SAAS,QAAQ,CAAC,KAAK,OAAO,KAAK,CAAC,IAAI,cAAc,CACjE,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAChD,OAAO,GAAG,EAAE;YACV,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACjC,IAAI,GAAG,IAAI,GAAG,CAAC,KAAK,KAAK,KAAK;gBAAE,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC1D,CAAC,CAAC;IACJ,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,IAAY;QAChB,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC;QAC/C,IAAI,KAAK;YAAE,OAAO,KAAK,CAAC;QACxB,IAAI,IAAuB,CAAC;QAC5B,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC;YACrC,IAAI,CAAC,CAAC,IAAI,KAAK,QAAQ;gBAAE,SAAS;YAClC,IAAI,IAAI,KAAK,CAAC,CAAC,IAAI,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;gBACrD,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM;oBAAE,IAAI,GAAG,CAAC,CAAC;YAC1D,CAAC;QACH,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;OAGG;IACH,aAAa;QACX,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;aAC7B,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC;aACzB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IAClE,CAAC;IAED,iBAAiB;IACjB,IAAI;QACF,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAC3C,KAAK,EAAE,CAAC,CAAC,KAAK;YACd,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,QAAQ,EAAE,CAAC,CAAC,QAAQ,KAAK,IAAI;SAC9B,CAAC,CAAC,CAAC;IACN,CAAC;CACF"}
|