@zhin.js/host-http 1.1.0 → 1.1.2
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 +5 -0
- package/lib/console-plugin-rpc.d.ts +3 -0
- package/lib/console-plugin-rpc.js +130 -0
- package/lib/console-rest-pages.d.ts +1 -1
- package/lib/console-rest-pages.js +11 -20
- package/lib/{console-rpc-extended.d.ts → console-rpc-extended/contracts.d.ts} +22 -21
- package/lib/console-rpc-extended/contracts.js +4 -0
- package/lib/console-rpc-extended/dispatcher.d.ts +2 -0
- package/lib/console-rpc-extended/dispatcher.js +92 -0
- package/lib/console-rpc-extended/endpoint-rpc.d.ts +24 -0
- package/lib/console-rpc-extended/endpoint-rpc.js +137 -0
- package/lib/console-rpc-extended/inbox-rpc.d.ts +23 -0
- package/lib/console-rpc-extended/inbox-rpc.js +292 -0
- package/lib/console-rpc-extended/index.d.ts +2 -0
- package/lib/console-rpc-extended/index.js +1 -0
- package/lib/console-rpc-extended/login-rpc.d.ts +4 -0
- package/lib/console-rpc-extended/login-rpc.js +46 -0
- package/lib/console-rpc-extended/rpc-values.d.ts +16 -0
- package/lib/console-rpc-extended/rpc-values.js +87 -0
- package/lib/console-rpc-extended/schedule-rpc.d.ts +6 -0
- package/lib/console-rpc-extended/schedule-rpc.js +109 -0
- package/lib/console-rpc-extended/workroom-rpc.d.ts +3 -0
- package/lib/console-rpc-extended/workroom-rpc.js +104 -0
- package/lib/console-rpc.d.ts +71 -15
- package/lib/console-rpc.js +116 -82
- package/lib/index.d.ts +3 -2
- package/lib/index.js +2 -1
- package/lib/marketplace-registry.d.ts +12 -0
- package/lib/marketplace-registry.js +45 -0
- package/package.json +4 -4
- package/src/console-plugin-rpc.ts +142 -0
- package/src/console-rest-pages.ts +14 -26
- package/src/console-rpc-extended/contracts.ts +155 -0
- package/src/console-rpc-extended/dispatcher.ts +114 -0
- package/src/console-rpc-extended/endpoint-rpc.ts +174 -0
- package/src/console-rpc-extended/inbox-rpc.ts +342 -0
- package/src/console-rpc-extended/index.ts +8 -0
- package/src/console-rpc-extended/login-rpc.ts +50 -0
- package/src/console-rpc-extended/rpc-values.ts +104 -0
- package/src/console-rpc-extended/schedule-rpc.ts +117 -0
- package/src/console-rpc-extended/workroom-rpc.ts +116 -0
- package/src/console-rpc.ts +189 -97
- package/src/index.ts +7 -2
- package/src/marketplace-registry.ts +58 -0
- package/lib/console-rpc-extended.js +0 -916
- package/src/console-rpc-extended.ts +0 -1192
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { ConsoleRpcExtendedCtx, ExtendedRpcResult } from './contracts.js';
|
|
2
|
+
import {
|
|
3
|
+
errorMessage, optionalRpcText, optionalRpcTextArray, requiredRpcBoolean, requiredRpcInteger,
|
|
4
|
+
requiredRpcText, requiredRpcValue,
|
|
5
|
+
} from './rpc-values.js';
|
|
6
|
+
|
|
7
|
+
export async function mutateWorkroomProfile(
|
|
8
|
+
type: 'workroom.profile.status' | 'workroom.profile.bootstrap'
|
|
9
|
+
| 'workroom.profile.pack.publish' | 'workroom.profile.publish'
|
|
10
|
+
| 'workroom.profile.rollback' | 'workroom.profile.policy.publish',
|
|
11
|
+
data: Record<string, unknown>,
|
|
12
|
+
ctx: ConsoleRpcExtendedCtx,
|
|
13
|
+
): Promise<ExtendedRpcResult> {
|
|
14
|
+
const control = ctx.workroomProfileControl;
|
|
15
|
+
if (!control) return { error: 'Workroom Profile authority control is unavailable' };
|
|
16
|
+
for (const forbidden of [
|
|
17
|
+
'authenticatedPrincipalId', 'principalId', 'decision', 'governance', 'approval', 'authority',
|
|
18
|
+
]) {
|
|
19
|
+
if (forbidden in data) return { error: `Console Workroom Profile request cannot carry ${forbidden}` };
|
|
20
|
+
}
|
|
21
|
+
if (type === 'workroom.profile.status') {
|
|
22
|
+
try {
|
|
23
|
+
return { data: await control.getPlanningStatus(
|
|
24
|
+
requiredRpcText(data.projectId, 'projectId'),
|
|
25
|
+
ctx.authenticatedPrincipal,
|
|
26
|
+
) };
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return { error: errorMessage(error) };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const principal = ctx.authenticatedPrincipal;
|
|
32
|
+
if (!principal) return { error: 'Workroom Profile control requires an authenticated principal' };
|
|
33
|
+
try {
|
|
34
|
+
const operationId = requiredRpcText(data.operationId, 'operationId');
|
|
35
|
+
if (type === 'workroom.profile.bootstrap') {
|
|
36
|
+
const projectId = requiredRpcText(data.projectId, 'projectId');
|
|
37
|
+
const includeTools = optionalRpcTextArray(data.includeTools, 'includeTools');
|
|
38
|
+
const includeSkills = optionalRpcTextArray(data.includeSkills, 'includeSkills');
|
|
39
|
+
return { data: await control.bootstrapPlanning(Object.freeze({
|
|
40
|
+
operationId,
|
|
41
|
+
projectId,
|
|
42
|
+
expectedRegistryRevision: requiredRpcInteger(
|
|
43
|
+
data.expectedRegistryRevision,
|
|
44
|
+
'expectedRegistryRevision',
|
|
45
|
+
-1,
|
|
46
|
+
),
|
|
47
|
+
...(includeTools ? { includeTools } : {}),
|
|
48
|
+
...(includeSkills ? { includeSkills } : {}),
|
|
49
|
+
}), principal) };
|
|
50
|
+
}
|
|
51
|
+
if (type === 'workroom.profile.pack.publish') {
|
|
52
|
+
return { data: await control.publishPack(Object.freeze({ operationId, pack: requiredRpcValue(data.pack, 'pack') }), principal) };
|
|
53
|
+
}
|
|
54
|
+
const projectId = requiredRpcText(data.projectId, 'projectId');
|
|
55
|
+
if (type === 'workroom.profile.policy.publish') {
|
|
56
|
+
const expectedPreviousDigest = optionalRpcText(data.expectedPreviousDigest, 'expectedPreviousDigest');
|
|
57
|
+
return { data: await control.publishPlanningPolicy(Object.freeze({
|
|
58
|
+
operationId,
|
|
59
|
+
projectId,
|
|
60
|
+
profileRevisionId: requiredRpcText(data.profileRevisionId, 'profileRevisionId'),
|
|
61
|
+
revision: requiredRpcInteger(data.revision, 'revision'),
|
|
62
|
+
...(expectedPreviousDigest ? { expectedPreviousDigest } : {}),
|
|
63
|
+
policy: requiredRpcValue(data.policy, 'policy'),
|
|
64
|
+
}), principal) };
|
|
65
|
+
}
|
|
66
|
+
const common = {
|
|
67
|
+
operationId,
|
|
68
|
+
projectId,
|
|
69
|
+
expectedRegistryRevision: requiredRpcInteger(data.expectedRegistryRevision, 'expectedRegistryRevision', -1),
|
|
70
|
+
overlay: requiredRpcValue(data.overlay, 'overlay'),
|
|
71
|
+
activate: requiredRpcBoolean(data.activate, 'activate'),
|
|
72
|
+
};
|
|
73
|
+
return type === 'workroom.profile.rollback'
|
|
74
|
+
? { data: await control.publishRollback(Object.freeze({
|
|
75
|
+
...common,
|
|
76
|
+
restoredFromRevisionId: requiredRpcText(data.restoredFromRevisionId, 'restoredFromRevisionId'),
|
|
77
|
+
}), principal) }
|
|
78
|
+
: { data: await control.publishProfile(Object.freeze(common), principal) };
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return { error: errorMessage(error) };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function mutateWorkroomKnowledge(
|
|
85
|
+
type: 'workroom.knowledge.get' | 'workroom.knowledge.publish' | 'workroom.knowledge.rollback',
|
|
86
|
+
data: Record<string, unknown>,
|
|
87
|
+
ctx: ConsoleRpcExtendedCtx,
|
|
88
|
+
): Promise<ExtendedRpcResult> {
|
|
89
|
+
const control = ctx.workroomKnowledgeControl;
|
|
90
|
+
if (!control) return { error: 'Workroom Knowledge control is unavailable' };
|
|
91
|
+
for (const forbidden of [
|
|
92
|
+
'authenticatedPrincipalId', 'principalId', 'ownerPrincipalId', 'source', 'decision',
|
|
93
|
+
'governance', 'approval', 'authority', 'body', 'content',
|
|
94
|
+
]) {
|
|
95
|
+
if (forbidden in data) return { error: `Console Workroom Knowledge request cannot carry ${forbidden}` };
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const projectId = requiredRpcText(data.projectId, 'projectId');
|
|
99
|
+
if (type === 'workroom.knowledge.get') return { data: await control.read(projectId) };
|
|
100
|
+
const principal = ctx.authenticatedPrincipal;
|
|
101
|
+
if (!principal) return { error: 'Workroom Knowledge mutation requires an authenticated principal' };
|
|
102
|
+
const common = {
|
|
103
|
+
operationId: requiredRpcText(data.operationId, 'operationId'), projectId,
|
|
104
|
+
expectedRevision: requiredRpcInteger(data.expectedRevision, 'expectedRevision', -1),
|
|
105
|
+
};
|
|
106
|
+
if (type === 'workroom.knowledge.rollback') {
|
|
107
|
+
return { data: await control.rollback(Object.freeze({
|
|
108
|
+
...common, restoreRevision: requiredRpcInteger(data.restoreRevision, 'restoreRevision', 0),
|
|
109
|
+
}), principal) };
|
|
110
|
+
}
|
|
111
|
+
if (!Array.isArray(data.entries)) throw new Error('entries is invalid');
|
|
112
|
+
return { data: await control.publish(Object.freeze({ ...common, entries: structuredClone(data.entries) }), principal) };
|
|
113
|
+
} catch (error) {
|
|
114
|
+
return { error: errorMessage(error) };
|
|
115
|
+
}
|
|
116
|
+
}
|
package/src/console-rpc.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { AuthScope } from './token-registry.js';
|
|
2
2
|
import type { DatabaseHostConsole } from '@zhin.js/plugin-runtime';
|
|
3
3
|
import {
|
|
4
|
+
CONFIG_RPC,
|
|
5
|
+
ENDPOINT_RPC,
|
|
6
|
+
type ConsoleConfigSource,
|
|
4
7
|
type ConsoleEndpointSummary,
|
|
5
8
|
assertDemoConsoleRpcAllowed,
|
|
6
|
-
endpointSendResult,
|
|
7
|
-
normalizeConsoleRpcMessage,
|
|
8
9
|
} from '@zhin.js/console-protocol';
|
|
9
|
-
import { dispatchExtendedConsoleRpc, type ConsoleRpcExtendedCtx } from './console-rpc-extended.js';
|
|
10
|
+
import { dispatchExtendedConsoleRpc, type ConsoleRpcExtendedCtx } from './console-rpc-extended/index.js';
|
|
11
|
+
import { dispatchPluginConsoleRpc } from './console-plugin-rpc.js';
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Console RPC 请求结构:未锚定 endpoint 的会话地址(结构对齐
|
|
@@ -23,17 +25,14 @@ export interface ConversationAddress {
|
|
|
23
25
|
readonly threadId?: string;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
/**
|
|
28
|
+
/** Canonical Console wire fields → runtime conversation address. */
|
|
27
29
|
function wireConversation(
|
|
28
30
|
channelType: string,
|
|
29
31
|
channelId: string,
|
|
30
32
|
parent: unknown,
|
|
31
33
|
): ConversationAddress {
|
|
32
|
-
// console 旧 wire 允许 channel_id 自带场景前缀(`group:123`),解析结果优先
|
|
33
|
-
const prefixed = /^(private|group|channel|direct|c2c|temp):(.+)$/iu.exec(channelId);
|
|
34
|
-
const rawKind = channelType || prefixed?.[1] || 'private';
|
|
35
34
|
const kind: ConversationAddress['kind'] =
|
|
36
|
-
|
|
35
|
+
channelType === 'group' || channelType === 'channel' ? channelType : 'private';
|
|
37
36
|
const wire = parent && typeof parent === 'object'
|
|
38
37
|
? parent as { readonly type?: unknown; readonly id?: unknown }
|
|
39
38
|
: undefined;
|
|
@@ -41,7 +40,7 @@ function wireConversation(
|
|
|
41
40
|
const parentId = typeof wire?.id === 'string' && wire.id ? wire.id : undefined;
|
|
42
41
|
return {
|
|
43
42
|
kind,
|
|
44
|
-
id:
|
|
43
|
+
id: channelId,
|
|
45
44
|
...(parentKind && parentId ? { parent: { kind: parentKind, id: parentId } } : {}),
|
|
46
45
|
};
|
|
47
46
|
}
|
|
@@ -54,6 +53,64 @@ export type RuntimeConsoleRpcMessage = {
|
|
|
54
53
|
|
|
55
54
|
export type RuntimeConsoleRpcReply = Record<string, unknown>;
|
|
56
55
|
|
|
56
|
+
export interface PluginInstallPlan {
|
|
57
|
+
readonly packageName: string;
|
|
58
|
+
readonly instanceKey: string;
|
|
59
|
+
readonly alreadyDeclared: boolean;
|
|
60
|
+
readonly alreadyInstalled: boolean;
|
|
61
|
+
readonly restartRequired: boolean;
|
|
62
|
+
readonly changes: Readonly<{
|
|
63
|
+
readonly packageManifest: 'unchanged' | 'add-plugin';
|
|
64
|
+
readonly config: 'unchanged' | 'create-entry' | 'schema-required';
|
|
65
|
+
}>;
|
|
66
|
+
readonly warnings: readonly string[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface PluginUninstallPlan {
|
|
70
|
+
readonly packageName: string;
|
|
71
|
+
readonly instanceKey: string;
|
|
72
|
+
readonly installed: boolean;
|
|
73
|
+
readonly declared: boolean;
|
|
74
|
+
readonly hasConfig: boolean;
|
|
75
|
+
readonly restartRequired: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface PluginUpdatePlan {
|
|
79
|
+
readonly packageName: string;
|
|
80
|
+
readonly instanceKey: string;
|
|
81
|
+
readonly currentVersion: string | null;
|
|
82
|
+
readonly targetVersion: string;
|
|
83
|
+
readonly installed: boolean;
|
|
84
|
+
readonly declared: boolean;
|
|
85
|
+
readonly alreadyCurrent: boolean;
|
|
86
|
+
readonly restartRequired: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface PluginManagementPort {
|
|
90
|
+
planInstall(packageName: string): Promise<PluginInstallPlan>;
|
|
91
|
+
install?(packageName: string, expectedRevision?: string): Promise<Readonly<{
|
|
92
|
+
readonly plan: PluginInstallPlan;
|
|
93
|
+
readonly restartRequired: boolean;
|
|
94
|
+
}>>;
|
|
95
|
+
planUninstall?(packageName: string): Promise<PluginUninstallPlan>;
|
|
96
|
+
uninstall?(packageName: string, expectedRevision?: string): Promise<Readonly<{
|
|
97
|
+
readonly plan: PluginUninstallPlan;
|
|
98
|
+
readonly restartRequired: boolean;
|
|
99
|
+
}>>;
|
|
100
|
+
planUpdate?(packageName: string, targetVersion: string): Promise<PluginUpdatePlan>;
|
|
101
|
+
update?(packageName: string, targetVersion: string, expectedRevision?: string): Promise<Readonly<{
|
|
102
|
+
readonly plan: PluginUpdatePlan;
|
|
103
|
+
readonly installedVersion: string;
|
|
104
|
+
readonly restartRequired: boolean;
|
|
105
|
+
}>>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface PluginConfigValidation {
|
|
109
|
+
readonly valid: boolean;
|
|
110
|
+
readonly errors: readonly { readonly path: string; readonly message: string }[];
|
|
111
|
+
readonly missingEnv: readonly string[];
|
|
112
|
+
}
|
|
113
|
+
|
|
57
114
|
export type RuntimeConsolePage = {
|
|
58
115
|
readonly id: string;
|
|
59
116
|
readonly localName: string;
|
|
@@ -68,17 +125,16 @@ export type RuntimeConsoleRpcContext = {
|
|
|
68
125
|
readonly authScope: AuthScope;
|
|
69
126
|
listPages(): Promise<readonly RuntimeConsolePage[]>;
|
|
70
127
|
/** Optional project config accessors for read-only config RPCs. */
|
|
71
|
-
|
|
72
|
-
listPluginKeys?(): Promise<readonly string[]>;
|
|
128
|
+
readConfigSource?(): Promise<ConsoleConfigSource>;
|
|
73
129
|
readConfigDocument?(): Promise<Record<string, unknown>>;
|
|
74
|
-
/**
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
130
|
+
/** Full-scope, revision-checked replacement of the active Root config source. */
|
|
131
|
+
replaceConfigSource?(
|
|
132
|
+
source: string,
|
|
133
|
+
expectedRevision: string,
|
|
134
|
+
): Promise<{ readonly revision: string; readonly restartRequired: boolean }>;
|
|
79
135
|
/**
|
|
80
136
|
* Full-scope write: set `document[pluginName] = data` and persist.
|
|
81
|
-
* Returns whether a process restart is required
|
|
137
|
+
* Returns whether a process restart is required.
|
|
82
138
|
*/
|
|
83
139
|
setConfigKey?(pluginName: string, data: unknown): Promise<{ restartRequired: boolean }>;
|
|
84
140
|
/** Persist a declared child Plugin lifecycle state. Takes effect after restart. */
|
|
@@ -86,6 +142,10 @@ export type RuntimeConsoleRpcContext = {
|
|
|
86
142
|
instanceKey: string,
|
|
87
143
|
enabled: boolean,
|
|
88
144
|
): Promise<Readonly<{ disabled: readonly string[] }>>;
|
|
145
|
+
/** Read-only install planning shared by Console Web and CLI surfaces. */
|
|
146
|
+
pluginManagement?: PluginManagementPort;
|
|
147
|
+
validatePluginConfig?(pluginName: string, data: unknown): Promise<PluginConfigValidation>;
|
|
148
|
+
diagnosePlugin?(pluginName: string): Promise<unknown>;
|
|
89
149
|
/** Atomic, revision-checked runtime Workroom Catalog read/write. */
|
|
90
150
|
readWorkroomCatalog?(): Promise<Readonly<{
|
|
91
151
|
agents: Readonly<Record<string, unknown>>;
|
|
@@ -109,8 +169,8 @@ export type RuntimeConsoleRpcContext = {
|
|
|
109
169
|
getSchema?(pluginName?: string): Promise<unknown>;
|
|
110
170
|
getAllSchemas?(): Promise<Record<string, unknown>>;
|
|
111
171
|
/** Optional Adapter endpoint Console surface (Sandbox / Remote Console). */
|
|
112
|
-
listEndpoints?(): Promise<readonly
|
|
113
|
-
getEndpoint?(adapter: string, endpointKey: string): Promise<
|
|
172
|
+
listEndpoints?(): Promise<readonly ConsoleEndpointSummary[]>;
|
|
173
|
+
getEndpoint?(adapter: string, endpointKey: string): Promise<ConsoleEndpointSummary | null>;
|
|
114
174
|
sendEndpointMessage?(input: RuntimeEndpointSendInput): Promise<{ messageId: string }>;
|
|
115
175
|
/** Optional Database host surface (CLI wires DatabaseHost from plugin-runtime). */
|
|
116
176
|
dbInfo?(): Promise<RuntimeDatabaseInfo> | RuntimeDatabaseInfo;
|
|
@@ -127,11 +187,6 @@ export type RuntimeConsoleRpcContext = {
|
|
|
127
187
|
extended?: Omit<ConsoleRpcExtendedCtx, 'fullScope'>;
|
|
128
188
|
};
|
|
129
189
|
|
|
130
|
-
export type RuntimeEndpointPhase = NonNullable<ConsoleEndpointSummary['phase']>;
|
|
131
|
-
|
|
132
|
-
/** @deprecated Prefer `ConsoleEndpointSummary` from `@zhin.js/console-protocol`. */
|
|
133
|
-
export type RuntimeEndpointSummary = ConsoleEndpointSummary;
|
|
134
|
-
|
|
135
190
|
export type RuntimeDatabaseInfo = {
|
|
136
191
|
readonly dialect: string | null;
|
|
137
192
|
readonly connected: boolean;
|
|
@@ -165,7 +220,6 @@ export async function dispatchRuntimeConsoleRpc(
|
|
|
165
220
|
const emit = (payload: RuntimeConsoleRpcReply) => {
|
|
166
221
|
payloads.push(payload);
|
|
167
222
|
};
|
|
168
|
-
message = normalizeConsoleRpcMessage(message);
|
|
169
223
|
const type = String(message.type ?? '');
|
|
170
224
|
const requestId = message.requestId as number | string | undefined;
|
|
171
225
|
|
|
@@ -192,6 +246,12 @@ export async function dispatchRuntimeConsoleRpc(
|
|
|
192
246
|
}
|
|
193
247
|
}
|
|
194
248
|
|
|
249
|
+
const pluginReply = await dispatchPluginConsoleRpc(type, message, ctx);
|
|
250
|
+
if (pluginReply !== undefined) {
|
|
251
|
+
emit(pluginReply);
|
|
252
|
+
return payloads;
|
|
253
|
+
}
|
|
254
|
+
|
|
195
255
|
switch (type) {
|
|
196
256
|
case 'ping':
|
|
197
257
|
emit({ type: 'pong', requestId });
|
|
@@ -217,11 +277,12 @@ export async function dispatchRuntimeConsoleRpc(
|
|
|
217
277
|
emit({ requestId, data: pages });
|
|
218
278
|
return payloads;
|
|
219
279
|
}
|
|
220
|
-
case
|
|
280
|
+
case CONFIG_RPC.GET_SOURCE: {
|
|
221
281
|
try {
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
282
|
+
const config: ConsoleConfigSource = ctx.readConfigSource
|
|
283
|
+
? await ctx.readConfigSource()
|
|
284
|
+
: { source: '', format: 'yaml', revision: '', configKeys: [] };
|
|
285
|
+
emit({ requestId, data: config });
|
|
225
286
|
} catch (error) {
|
|
226
287
|
emit({
|
|
227
288
|
requestId,
|
|
@@ -230,7 +291,7 @@ export async function dispatchRuntimeConsoleRpc(
|
|
|
230
291
|
}
|
|
231
292
|
return payloads;
|
|
232
293
|
}
|
|
233
|
-
case
|
|
294
|
+
case CONFIG_RPC.GET_ALL: {
|
|
234
295
|
try {
|
|
235
296
|
const document = ctx.readConfigDocument ? await ctx.readConfigDocument() : {};
|
|
236
297
|
emit({ requestId, data: document });
|
|
@@ -242,11 +303,9 @@ export async function dispatchRuntimeConsoleRpc(
|
|
|
242
303
|
}
|
|
243
304
|
return payloads;
|
|
244
305
|
}
|
|
245
|
-
case
|
|
306
|
+
case CONFIG_RPC.GET: {
|
|
246
307
|
try {
|
|
247
|
-
|
|
248
|
-
const data = (message.data ?? {}) as Record<string, unknown>;
|
|
249
|
-
const key = String(message.key ?? message.pluginName ?? data.pluginName ?? data.plugin ?? data.key ?? '');
|
|
308
|
+
const key = String(message.pluginName ?? '');
|
|
250
309
|
const document = ctx.readConfigDocument ? await ctx.readConfigDocument() : {};
|
|
251
310
|
emit({ requestId, data: key ? document[key] : document });
|
|
252
311
|
} catch (error) {
|
|
@@ -290,22 +349,33 @@ export async function dispatchRuntimeConsoleRpc(
|
|
|
290
349
|
}
|
|
291
350
|
return payloads;
|
|
292
351
|
}
|
|
293
|
-
case
|
|
352
|
+
case CONFIG_RPC.REPLACE_SOURCE: {
|
|
294
353
|
try {
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
354
|
+
const source = message.source;
|
|
355
|
+
const expectedRevision = message.expectedRevision;
|
|
356
|
+
if (typeof source !== 'string') {
|
|
357
|
+
emit({ requestId, error: 'source field is required' });
|
|
358
|
+
return payloads;
|
|
359
|
+
}
|
|
360
|
+
if (typeof expectedRevision !== 'string' || !/^[a-f0-9]{64}$/u.test(expectedRevision)) {
|
|
361
|
+
emit({ requestId, error: 'expectedRevision is required' });
|
|
298
362
|
return payloads;
|
|
299
363
|
}
|
|
300
|
-
if (!ctx.
|
|
364
|
+
if (!ctx.replaceConfigSource) {
|
|
301
365
|
emit({ requestId, error: 'Config write is not configured' });
|
|
302
366
|
return payloads;
|
|
303
367
|
}
|
|
304
|
-
await ctx.
|
|
368
|
+
const result = await ctx.replaceConfigSource(source, expectedRevision);
|
|
305
369
|
ctx.publishEvent?.('config:updated', { pluginName: null, keys: [] });
|
|
306
370
|
emit({
|
|
307
371
|
requestId,
|
|
308
|
-
data: {
|
|
372
|
+
data: {
|
|
373
|
+
success: true,
|
|
374
|
+
...result,
|
|
375
|
+
message: result.restartRequired
|
|
376
|
+
? '配置已保存,需重启进程才能生效'
|
|
377
|
+
: '配置已保存',
|
|
378
|
+
},
|
|
309
379
|
});
|
|
310
380
|
} catch (error) {
|
|
311
381
|
emit({
|
|
@@ -315,7 +385,7 @@ export async function dispatchRuntimeConsoleRpc(
|
|
|
315
385
|
}
|
|
316
386
|
return payloads;
|
|
317
387
|
}
|
|
318
|
-
case
|
|
388
|
+
case CONFIG_RPC.SET: {
|
|
319
389
|
try {
|
|
320
390
|
const pluginName = message.pluginName;
|
|
321
391
|
if (typeof pluginName !== 'string' || !pluginName) {
|
|
@@ -326,6 +396,17 @@ export async function dispatchRuntimeConsoleRpc(
|
|
|
326
396
|
emit({ requestId, error: 'Config write is not configured' });
|
|
327
397
|
return payloads;
|
|
328
398
|
}
|
|
399
|
+
if (ctx.validatePluginConfig) {
|
|
400
|
+
const validation = await ctx.validatePluginConfig(pluginName, message.data);
|
|
401
|
+
if (!validation.valid) {
|
|
402
|
+
emit({
|
|
403
|
+
requestId,
|
|
404
|
+
error: '配置校验失败',
|
|
405
|
+
data: validation,
|
|
406
|
+
});
|
|
407
|
+
return payloads;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
329
410
|
const result = await ctx.setConfigKey(pluginName, message.data);
|
|
330
411
|
ctx.publishEvent?.('config:updated', {
|
|
331
412
|
pluginName,
|
|
@@ -471,10 +552,9 @@ export async function dispatchRuntimeConsoleRpc(
|
|
|
471
552
|
}
|
|
472
553
|
case 'schema:get': {
|
|
473
554
|
try {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
const pluginName = typeof candidate === 'string' ? candidate : undefined;
|
|
555
|
+
const pluginName = typeof message.pluginName === 'string'
|
|
556
|
+
? message.pluginName
|
|
557
|
+
: undefined;
|
|
478
558
|
const schema = ctx.getSchema ? await ctx.getSchema(pluginName) : null;
|
|
479
559
|
emit({ requestId, data: schema });
|
|
480
560
|
} catch (error) {
|
|
@@ -678,11 +758,11 @@ export async function dispatchRuntimeConsoleRpc(
|
|
|
678
758
|
}
|
|
679
759
|
case 'endpoint.info': {
|
|
680
760
|
try {
|
|
681
|
-
const data = message
|
|
682
|
-
const adapter = String(data
|
|
683
|
-
const endpointKey = String(data
|
|
761
|
+
const data = recordField(message.data) ?? message;
|
|
762
|
+
const adapter = String(data.adapter ?? '');
|
|
763
|
+
const endpointKey = String(data.endpointKey ?? '');
|
|
684
764
|
if (!adapter || !endpointKey) {
|
|
685
|
-
emit({ requestId, error: '
|
|
765
|
+
emit({ requestId, error: 'adapter and endpointKey are required' });
|
|
686
766
|
return payloads;
|
|
687
767
|
}
|
|
688
768
|
if (!ctx.getEndpoint) {
|
|
@@ -703,19 +783,67 @@ export async function dispatchRuntimeConsoleRpc(
|
|
|
703
783
|
}
|
|
704
784
|
return payloads;
|
|
705
785
|
}
|
|
786
|
+
case ENDPOINT_RPC.TEST: {
|
|
787
|
+
const data = recordField(message.data) ?? message;
|
|
788
|
+
const adapter = stringField(data, 'adapter');
|
|
789
|
+
const endpointKey = stringField(data, 'endpointKey');
|
|
790
|
+
if (!adapter || !endpointKey) {
|
|
791
|
+
emit({ requestId, error: 'adapter and endpointKey are required' });
|
|
792
|
+
return payloads;
|
|
793
|
+
}
|
|
794
|
+
if (!ctx.getEndpoint) {
|
|
795
|
+
emit({ requestId, error: 'Endpoint registry is not configured' });
|
|
796
|
+
return payloads;
|
|
797
|
+
}
|
|
798
|
+
const startedAt = Date.now();
|
|
799
|
+
try {
|
|
800
|
+
const endpoint = await ctx.getEndpoint(adapter, endpointKey);
|
|
801
|
+
if (!endpoint) {
|
|
802
|
+
emit({ requestId, data: {
|
|
803
|
+
reachable: false,
|
|
804
|
+
connected: false,
|
|
805
|
+
phase: 'unconfigured',
|
|
806
|
+
latencyMs: Date.now() - startedAt,
|
|
807
|
+
message: 'Endpoint 未注册到当前 generation',
|
|
808
|
+
} });
|
|
809
|
+
return payloads;
|
|
810
|
+
}
|
|
811
|
+
emit({ requestId, data: {
|
|
812
|
+
reachable: endpoint.connected && endpoint.status === 'online',
|
|
813
|
+
connected: endpoint.connected,
|
|
814
|
+
status: endpoint.status,
|
|
815
|
+
phase: endpoint.phase ?? (endpoint.connected ? 'online' : 'pending'),
|
|
816
|
+
pendingLogin: endpoint.pendingLogin ?? false,
|
|
817
|
+
latencyMs: Date.now() - startedAt,
|
|
818
|
+
message: endpoint.connected
|
|
819
|
+
? 'Endpoint 已连接'
|
|
820
|
+
: endpoint.pendingLogin
|
|
821
|
+
? 'Endpoint 等待登录操作'
|
|
822
|
+
: 'Endpoint 当前离线',
|
|
823
|
+
} });
|
|
824
|
+
} catch (error) {
|
|
825
|
+
emit({ requestId, data: {
|
|
826
|
+
reachable: false,
|
|
827
|
+
connected: false,
|
|
828
|
+
phase: 'failed',
|
|
829
|
+
latencyMs: Date.now() - startedAt,
|
|
830
|
+
message: error instanceof Error ? error.message : String(error),
|
|
831
|
+
} });
|
|
832
|
+
}
|
|
833
|
+
return payloads;
|
|
834
|
+
}
|
|
706
835
|
case 'endpoint.send_message': {
|
|
707
836
|
try {
|
|
708
|
-
const data = message
|
|
709
|
-
const adapter = String(data
|
|
710
|
-
const endpointKey = String(data
|
|
711
|
-
const channelId = String(data
|
|
712
|
-
const channelType = String(data
|
|
713
|
-
const content = data
|
|
714
|
-
|
|
715
|
-
if (!adapter || !endpointKey || !channelId || content === undefined) {
|
|
837
|
+
const data = recordField(message.data) ?? message;
|
|
838
|
+
const adapter = String(data.adapter ?? '');
|
|
839
|
+
const endpointKey = String(data.endpointKey ?? '');
|
|
840
|
+
const channelId = String(data.channelId ?? data.id ?? '');
|
|
841
|
+
const channelType = String(data.channelType ?? data.type ?? '');
|
|
842
|
+
const content = data.content;
|
|
843
|
+
if (!adapter || !endpointKey || !channelId || !channelType || content === undefined) {
|
|
716
844
|
emit({
|
|
717
845
|
requestId,
|
|
718
|
-
error: '
|
|
846
|
+
error: 'adapter, endpointKey, channelId, channelType, and content are required',
|
|
719
847
|
});
|
|
720
848
|
return payloads;
|
|
721
849
|
}
|
|
@@ -726,11 +854,10 @@ export async function dispatchRuntimeConsoleRpc(
|
|
|
726
854
|
const result = await ctx.sendEndpointMessage({
|
|
727
855
|
adapter,
|
|
728
856
|
endpointKey,
|
|
729
|
-
conversation: wireConversation(channelType, channelId, data
|
|
857
|
+
conversation: wireConversation(channelType, channelId, data.parent),
|
|
730
858
|
content,
|
|
731
859
|
});
|
|
732
|
-
|
|
733
|
-
emit({ requestId, data: endpointSendResult(result.messageId) });
|
|
860
|
+
emit({ requestId, data: { messageId: result.messageId } });
|
|
734
861
|
} catch (error) {
|
|
735
862
|
emit({
|
|
736
863
|
requestId,
|
|
@@ -761,41 +888,6 @@ export async function dispatchRuntimeConsoleRpc(
|
|
|
761
888
|
}
|
|
762
889
|
return payloads;
|
|
763
890
|
}
|
|
764
|
-
case 'plugin:set-enabled': {
|
|
765
|
-
try {
|
|
766
|
-
const instanceKey = typeof message.instanceKey === 'string' ? message.instanceKey : '';
|
|
767
|
-
const enabled = message.enabled;
|
|
768
|
-
if (!instanceKey || typeof enabled !== 'boolean') {
|
|
769
|
-
emit({ requestId, error: 'instanceKey and boolean enabled are required' });
|
|
770
|
-
return payloads;
|
|
771
|
-
}
|
|
772
|
-
if (!ctx.setPluginEnabled) {
|
|
773
|
-
emit({ requestId, error: 'Plugin lifecycle management is not configured' });
|
|
774
|
-
return payloads;
|
|
775
|
-
}
|
|
776
|
-
const lifecycle = await ctx.setPluginEnabled(instanceKey, enabled);
|
|
777
|
-
ctx.publishEvent?.('plugin:lifecycle-updated', { instanceKey, enabled });
|
|
778
|
-
emit({
|
|
779
|
-
requestId,
|
|
780
|
-
data: {
|
|
781
|
-
success: true,
|
|
782
|
-
instanceKey,
|
|
783
|
-
enabled,
|
|
784
|
-
disabled: lifecycle.disabled,
|
|
785
|
-
restartRequired: true,
|
|
786
|
-
message: `${instanceKey} 已${enabled ? '启用' : '停用'},Host 正在重启`,
|
|
787
|
-
},
|
|
788
|
-
});
|
|
789
|
-
if (ctx.requestRestart) {
|
|
790
|
-
setTimeout(() => {
|
|
791
|
-
void Promise.resolve(ctx.requestRestart?.()).catch(() => undefined);
|
|
792
|
-
}, 500);
|
|
793
|
-
}
|
|
794
|
-
} catch (error) {
|
|
795
|
-
emit({ requestId, error: error instanceof Error ? error.message : String(error) });
|
|
796
|
-
}
|
|
797
|
-
return payloads;
|
|
798
|
-
}
|
|
799
891
|
default:
|
|
800
892
|
if (type.startsWith('db:')) {
|
|
801
893
|
emit({
|
package/src/index.ts
CHANGED
|
@@ -55,8 +55,13 @@ export {
|
|
|
55
55
|
type RuntimeConsoleRpcMessage,
|
|
56
56
|
type RuntimeConsoleRpcReply,
|
|
57
57
|
type RuntimeEndpointSendInput,
|
|
58
|
-
type
|
|
58
|
+
type PluginInstallPlan,
|
|
59
|
+
type PluginUninstallPlan,
|
|
60
|
+
type PluginUpdatePlan,
|
|
61
|
+
type PluginManagementPort,
|
|
62
|
+
type PluginConfigValidation,
|
|
59
63
|
} from './console-rpc.js';
|
|
64
|
+
export { dispatchPluginConsoleRpc } from './console-plugin-rpc.js';
|
|
60
65
|
export {
|
|
61
66
|
buildProjectFileTree,
|
|
62
67
|
FILE_MANAGER_ALLOWED,
|
|
@@ -82,4 +87,4 @@ export {
|
|
|
82
87
|
type ConsoleWorkroomProfileControlPort,
|
|
83
88
|
type ConsoleWorkroomKnowledgeControlPort,
|
|
84
89
|
type ExtendedRpcResult,
|
|
85
|
-
} from './console-rpc-extended.js';
|
|
90
|
+
} from './console-rpc-extended/index.js';
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export interface MarketplaceRegistryOptions {
|
|
2
|
+
readonly fetchFn: typeof fetch;
|
|
3
|
+
readonly url: string;
|
|
4
|
+
readonly ttlMs: number;
|
|
5
|
+
readonly now?: () => number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface MarketplaceSnapshot {
|
|
9
|
+
readonly plugins: readonly unknown[];
|
|
10
|
+
readonly expiresAt: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Host-owned plugin registry reader with TTL caching and request coalescing. */
|
|
14
|
+
export class MarketplaceRegistry {
|
|
15
|
+
readonly #fetch: typeof fetch;
|
|
16
|
+
readonly #url: string;
|
|
17
|
+
readonly #ttlMs: number;
|
|
18
|
+
readonly #now: () => number;
|
|
19
|
+
#snapshot?: MarketplaceSnapshot;
|
|
20
|
+
#inflight?: Promise<readonly unknown[]>;
|
|
21
|
+
|
|
22
|
+
constructor(options: MarketplaceRegistryOptions) {
|
|
23
|
+
this.#fetch = options.fetchFn;
|
|
24
|
+
this.#url = options.url;
|
|
25
|
+
this.#ttlMs = options.ttlMs;
|
|
26
|
+
this.#now = options.now ?? Date.now;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
list(): Promise<readonly unknown[]> {
|
|
30
|
+
const snapshot = this.#snapshot;
|
|
31
|
+
if (snapshot && this.#now() < snapshot.expiresAt) {
|
|
32
|
+
return Promise.resolve(snapshot.plugins);
|
|
33
|
+
}
|
|
34
|
+
if (this.#inflight) return this.#inflight;
|
|
35
|
+
|
|
36
|
+
const request = this.#load();
|
|
37
|
+
this.#inflight = request;
|
|
38
|
+
const clearInflight = () => {
|
|
39
|
+
if (this.#inflight === request) this.#inflight = undefined;
|
|
40
|
+
};
|
|
41
|
+
void request.then(clearInflight, clearInflight);
|
|
42
|
+
return request;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async #load(): Promise<readonly unknown[]> {
|
|
46
|
+
const response = await this.#fetch(this.#url, {
|
|
47
|
+
signal: AbortSignal.timeout(30_000),
|
|
48
|
+
});
|
|
49
|
+
if (!response.ok) throw new Error(`plugins.json fetch failed: ${response.status}`);
|
|
50
|
+
const json = (await response.json()) as { plugins?: unknown[] };
|
|
51
|
+
const plugins = Object.freeze([...(json.plugins ?? [])]);
|
|
52
|
+
this.#snapshot = Object.freeze({
|
|
53
|
+
plugins,
|
|
54
|
+
expiresAt: this.#now() + this.#ttlMs,
|
|
55
|
+
});
|
|
56
|
+
return plugins;
|
|
57
|
+
}
|
|
58
|
+
}
|