lume-dsh-plugin 0.7.2 → 0.7.3
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/lib/host/rpc-bridge.js +122 -0
- package/lib/index.js +43 -7
- package/package.json +1 -1
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection RPC 的**桥接协议**实现(纯函数 + 一个路由工厂)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要它:宿主 `connection.rpc.handle(channel, handler)` 内部要求
|
|
5
|
+
* 「频道注册属于**调用方 fiber**」(见 dsh-client-connection 的 `register(owner, …)` →
|
|
6
|
+
* `owner.effect(() => owner.webServer.register(route))`),在 DSH Desktop 0.9.1 上我们的
|
|
7
|
+
* 入口 fiber 解析不到 `webServer` → 抛 `cannot get property "webServer" without inject`。
|
|
8
|
+
* 于是准备一条**不依赖 connection.rpc 的回退路径**:照宿主自己的写法把 HTTP 路由挂到
|
|
9
|
+
* `webServer` 上(宿主自带的 dsh-ppt 就是这么做的),报文与客户端 `conn.rpc.call` 完全一致:
|
|
10
|
+
*
|
|
11
|
+
* 请求 POST {channel}/{endpoint} content-type: application/json
|
|
12
|
+
* { type: "client-request", rpcId, method: endpoint, payload }
|
|
13
|
+
* 响应 200 { type: "server-response", rpcId, result: { ok, value|error } }
|
|
14
|
+
*
|
|
15
|
+
* 纯函数部分(解析/组装)可单测,路由工厂只做 Node req/res 的胶水。
|
|
16
|
+
*/
|
|
17
|
+
/** 端点段白名单,与宿主 ENDPOINT_SEGMENT_PATTERN 一致。 */
|
|
18
|
+
const ENDPOINT_SEGMENT_PATTERN = /^[A-Za-z0-9_$.-]+$/;
|
|
19
|
+
/** 组装一条回信(含 rpcId 与错误信封补全)。 */
|
|
20
|
+
export function connectionResponse(rpcId, result) {
|
|
21
|
+
return JSON.stringify({ type: "server-response", rpcId, result: withErrorDetails(result) });
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 补全错误信封:客户端的 `parseConnectionResponse` 要求失败时
|
|
25
|
+
* `error.details` 是对象,缺失会直接抛 "invalid server-response failure"。
|
|
26
|
+
* (这是 0.7.2 之前一直存在的隐患:任何错误路径都会让客户端调用炸掉。)
|
|
27
|
+
*/
|
|
28
|
+
export function withErrorDetails(result) {
|
|
29
|
+
const envelope = result;
|
|
30
|
+
if (!envelope || typeof envelope !== "object" || envelope.ok !== false)
|
|
31
|
+
return result;
|
|
32
|
+
const error = envelope.error ?? {};
|
|
33
|
+
return {
|
|
34
|
+
ok: false,
|
|
35
|
+
error: {
|
|
36
|
+
code: typeof error.code === "string" ? error.code : "bad-request",
|
|
37
|
+
message: typeof error.message === "string" ? error.message : "unknown error",
|
|
38
|
+
details: error.details && typeof error.details === "object" ? error.details : {},
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** 解析客户端请求。与宿主的 rpcFetchHandler 语义对齐(404/415/400 + 信封校验)。 */
|
|
43
|
+
export function parseClientRequest(channel, request) {
|
|
44
|
+
if ((request.method ?? "GET").toUpperCase() !== "POST")
|
|
45
|
+
return { kind: "respond", status: 404, body: "not found", contentType: "text/plain" };
|
|
46
|
+
const pathname = (request.url ?? "/").split("?")[0] ?? "/";
|
|
47
|
+
if (!pathname.startsWith(`${channel}/`))
|
|
48
|
+
return { kind: "respond", status: 404, body: "not found", contentType: "text/plain" };
|
|
49
|
+
const endpoint = pathname.slice(channel.length + 1);
|
|
50
|
+
if (endpoint.split("/").some((segment) => segment === "" || segment === "." || segment === ".." || !ENDPOINT_SEGMENT_PATTERN.test(segment))) {
|
|
51
|
+
return { kind: "respond", status: 404, body: "not found", contentType: "text/plain" };
|
|
52
|
+
}
|
|
53
|
+
const mime = (request.contentType ?? "").split(";")[0]?.trim().toLowerCase();
|
|
54
|
+
if (mime !== "application/json")
|
|
55
|
+
return { kind: "respond", status: 415, body: "content type must be application/json", contentType: "text/plain" };
|
|
56
|
+
let body;
|
|
57
|
+
try {
|
|
58
|
+
body = JSON.parse(request.rawBody);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return { kind: "respond", status: 400, body: "body is not JSON", contentType: "text/plain" };
|
|
62
|
+
}
|
|
63
|
+
const envelope = body;
|
|
64
|
+
if (!envelope || typeof envelope !== "object" || envelope.type !== "client-request" || typeof envelope.rpcId !== "string" || typeof envelope.method !== "string") {
|
|
65
|
+
const rpcId = typeof envelope?.rpcId === "string" ? String(envelope.rpcId) : "invalid-request";
|
|
66
|
+
return { kind: "respond", status: 200, body: connectionResponse(rpcId, { ok: false, error: { code: "gateway/bad-request", message: "invalid client-request message" } }), contentType: "application/json" };
|
|
67
|
+
}
|
|
68
|
+
if (envelope.method !== endpoint) {
|
|
69
|
+
return { kind: "respond", status: 200, body: connectionResponse(envelope.rpcId, { ok: false, error: { code: "gateway/bad-request", message: `endpoint mismatch: ${envelope.method} vs ${endpoint}` } }), contentType: "application/json" };
|
|
70
|
+
}
|
|
71
|
+
return { kind: "dispatch", rpcId: envelope.rpcId, endpoint, payload: envelope.payload };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 组装可交给 `webServer.register()` 的路由(宿主自带 dsh-ppt 同形:
|
|
75
|
+
* `{ kind: "prefix", path: channel, handler(req, res) }`)。
|
|
76
|
+
*
|
|
77
|
+
* @param channel - RPC 频道(须匹配 /^\/[A-Za-z0-9._~-]+$/,且不得为 /api)
|
|
78
|
+
* @param dispatch - 业务分发(endpoint + payload → lume 信封)
|
|
79
|
+
* @param guard - 可选的浏览器信任闸(宿主 connection.requestRejection),返回状态码即拒绝
|
|
80
|
+
*/
|
|
81
|
+
export function makeRpcRoute(channel, dispatch, guard) {
|
|
82
|
+
return {
|
|
83
|
+
kind: "prefix",
|
|
84
|
+
path: channel,
|
|
85
|
+
handler: async (req, res) => {
|
|
86
|
+
const rejection = guard?.(req);
|
|
87
|
+
if (rejection !== undefined) {
|
|
88
|
+
res.writeHead(rejection);
|
|
89
|
+
res.end(rejection === 401 ? "unauthorized" : "forbidden");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
let rawBody = "";
|
|
93
|
+
try {
|
|
94
|
+
for await (const chunk of req)
|
|
95
|
+
rawBody += typeof chunk === "string" ? chunk : String(chunk);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
/* 客户端断开:下面按空 body 处理并回 400 */
|
|
99
|
+
}
|
|
100
|
+
const parsed = parseClientRequest(channel, {
|
|
101
|
+
method: req?.method,
|
|
102
|
+
url: req?.url,
|
|
103
|
+
contentType: req?.headers?.["content-type"] ?? null,
|
|
104
|
+
rawBody,
|
|
105
|
+
});
|
|
106
|
+
if (parsed.kind === "respond") {
|
|
107
|
+
res.writeHead(parsed.status, { "content-type": parsed.contentType });
|
|
108
|
+
res.end(parsed.body);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
let result;
|
|
112
|
+
try {
|
|
113
|
+
result = await dispatch(parsed.endpoint, parsed.payload);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
result = { ok: false, error: { code: "internal", message: String(error?.message ?? error) } };
|
|
117
|
+
}
|
|
118
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
119
|
+
res.end(connectionResponse(parsed.rpcId, result));
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -21,6 +21,7 @@ import z from "@deepseek-ai/schemastery";
|
|
|
21
21
|
import { buildPersonaContractSection, buildPersonaRuntimeSection } from "./host/injection.js";
|
|
22
22
|
import { loadPersonalities, NONE_PERSONA } from "./host/personalities.js";
|
|
23
23
|
import { createLumeRpcHandler } from "./host/rpc.js";
|
|
24
|
+
import { makeRpcRoute } from "./host/rpc-bridge.js";
|
|
24
25
|
import { FilePersonaStore, migrateLegacyState, PersonaStore } from "./host/store.js";
|
|
25
26
|
import { IdentityStore, LUME_IDENTITY_SPEC, zodLike } from "./host/identity.js";
|
|
26
27
|
import { PersonaRegistry } from "./host/registry.js";
|
|
@@ -1290,22 +1291,57 @@ function applyInner(ctx, config = {}) {
|
|
|
1290
1291
|
* inject 回调里调用(不包 effect)。没有 web 载体的宿主(headless)只是没有 RPC 通道。
|
|
1291
1292
|
*/
|
|
1292
1293
|
function registerRpcChannel(scope) {
|
|
1293
|
-
|
|
1294
|
+
const dispatch = async (endpoint, payload) => {
|
|
1295
|
+
currentStore ??= await storesReady;
|
|
1296
|
+
identity ??= await identityReady;
|
|
1297
|
+
return handleEndpoint(endpoint, payload);
|
|
1298
|
+
};
|
|
1299
|
+
// 依赖组合与宿主自带 dsh-api-gateway 一致(["connection", "webServer"])。
|
|
1300
|
+
scope.inject(["connection", "webServer"], (webCtx) => {
|
|
1301
|
+
const notes = [];
|
|
1294
1302
|
try {
|
|
1303
|
+
// ── 主路径:宿主公开 API。频道注册归属**调用方 fiber**,所以必须在注入作用域里调用。
|
|
1295
1304
|
webCtx.connection.rpc.handle(LUME_CHANNEL, async (endpoint, payload) => {
|
|
1296
|
-
|
|
1297
|
-
identity ??= await identityReady;
|
|
1298
|
-
const result = await handleEndpoint(endpoint, payload);
|
|
1305
|
+
const result = await dispatch(endpoint, payload);
|
|
1299
1306
|
if (endpoint !== "list" && endpoint !== "getSessionPersona") {
|
|
1300
|
-
webCtx.logger?.warn?.(`lume: rpc ${endpoint}
|
|
1307
|
+
webCtx.logger?.warn?.(`lume: rpc ${endpoint} → ok=${result.ok}${result.ok ? "" : ` code=${result.error.code}`}`);
|
|
1301
1308
|
}
|
|
1302
1309
|
return result;
|
|
1303
1310
|
}, { authority: "trusted-host" });
|
|
1311
|
+
notes.push("connection.rpc.handle");
|
|
1304
1312
|
}
|
|
1305
1313
|
catch (error) {
|
|
1306
|
-
|
|
1307
|
-
|
|
1314
|
+
notes.push(`rpc.handle 失败(${describeError(error)})`);
|
|
1315
|
+
// ── 回退:自己往 webServer 注册 HTTP 路由(宿主自带 dsh-ppt 的写法)。
|
|
1316
|
+
// 报文与客户端 conn.rpc.call 完全一致,见 host/rpc-bridge.ts。
|
|
1317
|
+
try {
|
|
1318
|
+
const guard = typeof webCtx.connection?.requestRejection === "function"
|
|
1319
|
+
? (req) => webCtx.connection.requestRejection(req)
|
|
1320
|
+
: undefined;
|
|
1321
|
+
webCtx.webServer.register(makeRpcRoute(LUME_CHANNEL, dispatch, guard));
|
|
1322
|
+
notes.push("webServer.register(自注册路由)");
|
|
1323
|
+
}
|
|
1324
|
+
catch (fallbackError) {
|
|
1325
|
+
notes.push(`webServer.register 失败(${describeError(fallbackError)})`);
|
|
1326
|
+
}
|
|
1308
1327
|
}
|
|
1328
|
+
// 两条都失败时补上环境形状,便于下一轮定位(宿主 API 变更时这行就是证据)。
|
|
1329
|
+
if (!notes.some((note) => !note.includes("失败"))) {
|
|
1330
|
+
notes.push(`shapes: connection=${typeof webCtx.connection} rpc=${typeof webCtx.connection?.rpc} handle=${typeof webCtx.connection?.rpc?.handle} webServer=${typeof webCtx.webServer} get=${typeof webCtx.get}`);
|
|
1331
|
+
}
|
|
1332
|
+
webCtx.logger?.warn?.(`lume: RPC 通道 ${LUME_CHANNEL} = ${notes.join(" | ")}`);
|
|
1309
1333
|
});
|
|
1310
1334
|
}
|
|
1335
|
+
/** 把任意抛出物变成可读的一行(宿主 logger 直接打对象会变成 `{}`,所以要自己转字符串)。 */
|
|
1336
|
+
function describeError(error) {
|
|
1337
|
+
if (error instanceof Error)
|
|
1338
|
+
return error.message;
|
|
1339
|
+
try {
|
|
1340
|
+
const text = JSON.stringify(error);
|
|
1341
|
+
return text && text !== "{}" ? text : String(error);
|
|
1342
|
+
}
|
|
1343
|
+
catch {
|
|
1344
|
+
return String(error);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1311
1347
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lume-dsh-plugin",
|
|
3
3
|
"description": "微光 (Lume) — DSH Desktop 增强插件:给会话装上工程纪律与真实关系。纪律层约束「如何正确完成任务」——意图路由、阶段门控、真实工具证据、交付前复核、文档能力感知;方法层把量化需求、改动台账、假设台账与项目知识变成可检查的产出,并用行为触发器在轨迹上纠偏(撒网不收敛 / 连写不验 / 死路重撞);人设层塑造「以何种风格表达」——从聊天记录、小说、剧本、设定文档蒸馏具名角色,长期记忆与风格随对话演进。约束按需注入,闲聊不额外付 token。",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|