@zhin.js/client 2.0.3 → 2.0.5
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/client/agent-stream.ts +121 -0
- package/client/bootstrap/loadConsoleEntries.ts +1 -7
- package/client/console-utils/remoteApi.ts +13 -2
- package/client/index.ts +16 -0
- package/client/mediaSrc.ts +6 -1
- package/client/orchestration/OrchestrationRunsPage.tsx +217 -0
- package/client/orchestration/registerOrchestrationConsole.test.ts +26 -0
- package/client/orchestration/registerOrchestrationConsole.ts +17 -0
- package/client/persistence/idb-store.ts +16 -10
- package/client/segments.test.ts +16 -0
- package/client/segments.ts +39 -0
- package/client/types.ts +5 -5
- package/client/websocket/manager.ts +3 -13
- package/client/websocket/remote-settings.ts +13 -3
- package/dist/agent-stream.d.ts +17 -0
- package/dist/agent-stream.d.ts.map +1 -0
- package/dist/agent-stream.js +73 -0
- package/dist/agent-stream.js.map +1 -0
- package/dist/bootstrap/loadConsoleEntries.d.ts +1 -1
- package/dist/bootstrap/loadConsoleEntries.d.ts.map +1 -1
- package/dist/bootstrap/loadConsoleEntries.js +1 -1
- package/dist/bootstrap/loadConsoleEntries.js.map +1 -1
- package/dist/console-utils/remoteApi.d.ts.map +1 -1
- package/dist/console-utils/remoteApi.js +12 -2
- package/dist/console-utils/remoteApi.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/mediaSrc.d.ts +1 -1
- package/dist/mediaSrc.d.ts.map +1 -1
- package/dist/mediaSrc.js +7 -1
- package/dist/mediaSrc.js.map +1 -1
- package/dist/orchestration/OrchestrationRunsPage.d.ts +2 -0
- package/dist/orchestration/OrchestrationRunsPage.d.ts.map +1 -0
- package/dist/orchestration/OrchestrationRunsPage.js +65 -0
- package/dist/orchestration/OrchestrationRunsPage.js.map +1 -0
- package/dist/orchestration/registerOrchestrationConsole.d.ts +4 -0
- package/dist/orchestration/registerOrchestrationConsole.d.ts.map +1 -0
- package/dist/orchestration/registerOrchestrationConsole.js +16 -0
- package/dist/orchestration/registerOrchestrationConsole.js.map +1 -0
- package/dist/persistence/idb-store.d.ts +2 -2
- package/dist/persistence/idb-store.d.ts.map +1 -1
- package/dist/persistence/idb-store.js +12 -9
- package/dist/persistence/idb-store.js.map +1 -1
- package/dist/segments.d.ts +9 -0
- package/dist/segments.d.ts.map +1 -0
- package/dist/segments.js +35 -0
- package/dist/segments.js.map +1 -0
- package/dist/types.d.ts +4 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/websocket/manager.d.ts +1 -2
- package/dist/websocket/manager.d.ts.map +1 -1
- package/dist/websocket/manager.js +2 -2
- package/dist/websocket/manager.js.map +1 -1
- package/dist/websocket/remote-settings.js +12 -3
- package/dist/websocket/remote-settings.js.map +1 -1
- package/package.json +13 -4
- package/dist/bootstrap/loadConsoleEntries.test.d.ts +0 -2
- package/dist/bootstrap/loadConsoleEntries.test.d.ts.map +0 -1
- package/dist/bootstrap/loadConsoleEntries.test.js +0 -27
- package/dist/bootstrap/loadConsoleEntries.test.js.map +0 -1
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AGENT_STREAM_MEDIA_TYPE,
|
|
3
|
+
ZHIN_AGENT_SESSION_API_PREFIX,
|
|
4
|
+
ZHIN_SESSION_ID_HEADER,
|
|
5
|
+
foldAgentStreamNdjson,
|
|
6
|
+
iterateAgentStreamNdjson,
|
|
7
|
+
type AgentStreamEvent,
|
|
8
|
+
type AgentStreamReduceState,
|
|
9
|
+
type ContinueAgentSessionResponse,
|
|
10
|
+
type FoldAgentStreamOptions,
|
|
11
|
+
type StartAgentSessionResponse,
|
|
12
|
+
} from "@zhin.js/contract";
|
|
13
|
+
import { getToken, resolveApiUrl } from "./console-utils/remoteApi.js";
|
|
14
|
+
|
|
15
|
+
export type AgentStreamClientOptions = {
|
|
16
|
+
fetch?: typeof fetch;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function resolveFetch(options?: AgentStreamClientOptions): typeof fetch {
|
|
20
|
+
const impl = options?.fetch ?? globalThis.fetch;
|
|
21
|
+
if (!impl) throw new Error("fetch is not available");
|
|
22
|
+
return impl;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function authedFetch(
|
|
26
|
+
path: string,
|
|
27
|
+
init: RequestInit,
|
|
28
|
+
fetchImpl: typeof fetch,
|
|
29
|
+
): Promise<Response> {
|
|
30
|
+
const token = getToken();
|
|
31
|
+
const headers = new Headers(init.headers);
|
|
32
|
+
if (token) headers.set("Authorization", `Bearer ${token}`);
|
|
33
|
+
const url = path.startsWith("/") ? resolveApiUrl(path) : path;
|
|
34
|
+
const res = await fetchImpl(url, { ...init, headers });
|
|
35
|
+
if (res.status === 401 && typeof localStorage !== "undefined") {
|
|
36
|
+
sessionStorage.removeItem("zhin_api_token");
|
|
37
|
+
localStorage.removeItem("zhin_api_token");
|
|
38
|
+
window.dispatchEvent(new CustomEvent("zhin:auth-required"));
|
|
39
|
+
}
|
|
40
|
+
return res;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function readJson<T>(res: Response): Promise<T> {
|
|
44
|
+
if (!res.ok) {
|
|
45
|
+
const err = await res.json().catch(() => ({})) as { error?: string };
|
|
46
|
+
throw new Error(err.error ?? `HTTP ${res.status}`);
|
|
47
|
+
}
|
|
48
|
+
return res.json() as Promise<T>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** POST /zhin/v1/session — start a Host agent session turn. */
|
|
52
|
+
export async function startAgentSession(
|
|
53
|
+
message: string,
|
|
54
|
+
options?: AgentStreamClientOptions,
|
|
55
|
+
): Promise<StartAgentSessionResponse> {
|
|
56
|
+
const fetchImpl = resolveFetch(options);
|
|
57
|
+
const res = await authedFetch(`${ZHIN_AGENT_SESSION_API_PREFIX}/session`, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: { "Content-Type": "application/json" },
|
|
60
|
+
body: JSON.stringify({ message }),
|
|
61
|
+
}, fetchImpl);
|
|
62
|
+
return readJson<StartAgentSessionResponse>(res);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** POST /zhin/v1/session/:sessionId — continue with continuationToken + message. */
|
|
66
|
+
export async function continueAgentSession(
|
|
67
|
+
sessionId: string,
|
|
68
|
+
continuationToken: string,
|
|
69
|
+
message: string,
|
|
70
|
+
options?: AgentStreamClientOptions,
|
|
71
|
+
): Promise<ContinueAgentSessionResponse> {
|
|
72
|
+
const fetchImpl = resolveFetch(options);
|
|
73
|
+
const res = await authedFetch(
|
|
74
|
+
`${ZHIN_AGENT_SESSION_API_PREFIX}/session/${encodeURIComponent(sessionId)}`,
|
|
75
|
+
{
|
|
76
|
+
method: "POST",
|
|
77
|
+
headers: { "Content-Type": "application/json" },
|
|
78
|
+
body: JSON.stringify({ continuationToken, message }),
|
|
79
|
+
},
|
|
80
|
+
fetchImpl,
|
|
81
|
+
);
|
|
82
|
+
return readJson<ContinueAgentSessionResponse>(res);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type SubscribeAgentStreamOptions = FoldAgentStreamOptions & {
|
|
86
|
+
startIndex?: number;
|
|
87
|
+
fetch?: typeof fetch;
|
|
88
|
+
onEvent?: (event: AgentStreamEvent, state: AgentStreamReduceState) => void;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** GET /zhin/v1/session/:sessionId/stream — consume NDJSON until the stream closes. */
|
|
92
|
+
export async function subscribeAgentStream(
|
|
93
|
+
sessionId: string,
|
|
94
|
+
options?: SubscribeAgentStreamOptions,
|
|
95
|
+
): Promise<AgentStreamReduceState> {
|
|
96
|
+
const fetchImpl = resolveFetch(options);
|
|
97
|
+
const startIndex = options?.startIndex ?? 0;
|
|
98
|
+
const query = startIndex > 0 ? `?startIndex=${startIndex}` : "";
|
|
99
|
+
const res = await authedFetch(
|
|
100
|
+
`${ZHIN_AGENT_SESSION_API_PREFIX}/session/${encodeURIComponent(sessionId)}/stream${query}`,
|
|
101
|
+
{
|
|
102
|
+
method: "GET",
|
|
103
|
+
headers: { Accept: AGENT_STREAM_MEDIA_TYPE },
|
|
104
|
+
},
|
|
105
|
+
fetchImpl,
|
|
106
|
+
);
|
|
107
|
+
if (!res.ok) {
|
|
108
|
+
const err = await res.json().catch(() => ({})) as { error?: string };
|
|
109
|
+
throw new Error(err.error ?? `HTTP ${res.status}`);
|
|
110
|
+
}
|
|
111
|
+
const headerSessionId = res.headers.get(ZHIN_SESSION_ID_HEADER);
|
|
112
|
+
if (headerSessionId && headerSessionId !== sessionId) {
|
|
113
|
+
throw new Error(`stream session id mismatch: ${headerSessionId}`);
|
|
114
|
+
}
|
|
115
|
+
if (!res.body) {
|
|
116
|
+
throw new Error("stream response has no body");
|
|
117
|
+
}
|
|
118
|
+
return foldAgentStreamNdjson(res.body, options);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export { iterateAgentStreamNdjson, foldAgentStreamNdjson };
|
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
import type
|
|
2
|
-
ConsoleClientEntry,
|
|
3
|
-
ConsoleEntriesResponse,
|
|
4
|
-
ConsolePluginRegister,
|
|
5
|
-
PluginRegisterHostApi,
|
|
6
|
-
} from "@zhin.js/contract";
|
|
7
|
-
import { DEFAULT_CONSOLE_BASE_PATH } from "@zhin.js/contract";
|
|
1
|
+
import { DEFAULT_CONSOLE_BASE_PATH, type ConsoleClientEntry, type ConsoleEntriesResponse, type ConsolePluginRegister, type PluginRegisterHostApi } from '@zhin.js/contract';
|
|
8
2
|
|
|
9
3
|
export type FetchConsoleEntriesOptions = {
|
|
10
4
|
entriesUrl?: string;
|
|
@@ -3,13 +3,13 @@ const API_BASE_KEY = "zhin_api_base";
|
|
|
3
3
|
|
|
4
4
|
export function getApiBase(): string {
|
|
5
5
|
const stored = localStorage.getItem(API_BASE_KEY)?.trim();
|
|
6
|
-
if (stored) return stored
|
|
6
|
+
if (stored) return trimTrailingSlash(stored);
|
|
7
7
|
if (typeof window !== "undefined") return window.location.origin;
|
|
8
8
|
return "";
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export function getToken(): string | null {
|
|
12
|
-
return localStorage.getItem(TOKEN_KEY);
|
|
12
|
+
return getRuntimeToken() ?? localStorage.getItem(TOKEN_KEY);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export function resolveApiUrl(path: string): string {
|
|
@@ -41,8 +41,19 @@ export async function apiFetch(
|
|
|
41
41
|
|
|
42
42
|
const res = await fetch(url, { ...init, headers });
|
|
43
43
|
if (res.status === 401) {
|
|
44
|
+
sessionStorage.removeItem(TOKEN_KEY);
|
|
44
45
|
localStorage.removeItem(TOKEN_KEY);
|
|
45
46
|
window.dispatchEvent(new CustomEvent("zhin:auth-required"));
|
|
46
47
|
}
|
|
47
48
|
return res;
|
|
48
49
|
}
|
|
50
|
+
|
|
51
|
+
function trimTrailingSlash(value: string): string {
|
|
52
|
+
return value.endsWith("/") ? value.slice(0, -1) : value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getRuntimeToken(): string | null {
|
|
56
|
+
if (typeof window === "undefined") return null;
|
|
57
|
+
const token = (window as unknown as { __ZHIN_API_TOKEN?: string }).__ZHIN_API_TOKEN;
|
|
58
|
+
return token?.trim() || null;
|
|
59
|
+
}
|
package/client/index.ts
CHANGED
|
@@ -4,6 +4,9 @@ export * from "./types";
|
|
|
4
4
|
// Media URL resolution
|
|
5
5
|
export { resolveMediaSrc, pickMediaRawUrl, type MediaKind } from "./mediaSrc";
|
|
6
6
|
|
|
7
|
+
// Segment IM visibility (inbox vs agent panel)
|
|
8
|
+
export { segmentsForImDelivery, segmentsForAgentPanel } from "./segments.js";
|
|
9
|
+
|
|
7
10
|
// Console app singleton (pagemanager / registry style)
|
|
8
11
|
export {
|
|
9
12
|
app,
|
|
@@ -47,3 +50,16 @@ export {
|
|
|
47
50
|
type FetchConsoleEntriesOptions,
|
|
48
51
|
type LoadConsoleEntriesOptions,
|
|
49
52
|
} from "./bootstrap/loadConsoleEntries.js";
|
|
53
|
+
|
|
54
|
+
export { registerOrchestrationConsole } from "./orchestration/registerOrchestrationConsole.js";
|
|
55
|
+
export { default as OrchestrationRunsPage } from "./orchestration/OrchestrationRunsPage.js";
|
|
56
|
+
|
|
57
|
+
export {
|
|
58
|
+
startAgentSession,
|
|
59
|
+
continueAgentSession,
|
|
60
|
+
subscribeAgentStream,
|
|
61
|
+
iterateAgentStreamNdjson,
|
|
62
|
+
foldAgentStreamNdjson,
|
|
63
|
+
type AgentStreamClientOptions,
|
|
64
|
+
type SubscribeAgentStreamOptions,
|
|
65
|
+
} from "./agent-stream.js";
|
package/client/mediaSrc.ts
CHANGED
|
@@ -43,9 +43,14 @@ export function resolveMediaSrc(
|
|
|
43
43
|
return `data:${defaultMime};base64,${b64}`
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
/** 从 segment.data
|
|
46
|
+
/** 从 segment.data 取常见媒体字段(优先 canonical MediaRef) */
|
|
47
47
|
export function pickMediaRawUrl(data: Record<string, unknown> | undefined): string | undefined {
|
|
48
48
|
if (!data) return undefined
|
|
49
|
+
const media = data.media
|
|
50
|
+
if (media && typeof media === 'object' && media !== null) {
|
|
51
|
+
const value = (media as { value?: unknown }).value
|
|
52
|
+
if (typeof value === 'string' && value.trim()) return value.trim()
|
|
53
|
+
}
|
|
49
54
|
const v = data.url ?? data.file ?? data.src ?? data.href
|
|
50
55
|
return typeof v === 'string' && v.trim() ? v.trim() : undefined
|
|
51
56
|
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
import { apiFetch } from '../console-utils/remoteApi.js';
|
|
3
|
+
|
|
4
|
+
type OrchestrationTaskRow = {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
status: string;
|
|
8
|
+
executor_kind?: string;
|
|
9
|
+
assigned_to?: string;
|
|
10
|
+
result_summary?: string;
|
|
11
|
+
error?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type OrchestrationEventRow = {
|
|
15
|
+
seq: number;
|
|
16
|
+
type: string;
|
|
17
|
+
task_id?: string;
|
|
18
|
+
payload_json?: string;
|
|
19
|
+
created_at?: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type RunListItem = {
|
|
23
|
+
run: { id: string; session_key: string; status: string; title: string };
|
|
24
|
+
tasks: OrchestrationTaskRow[];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type RunDetail = RunListItem & {
|
|
28
|
+
events?: OrchestrationEventRow[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function parsePayload(raw?: string): string {
|
|
32
|
+
if (!raw?.trim()) return '';
|
|
33
|
+
try {
|
|
34
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
35
|
+
return JSON.stringify(parsed, null, 2);
|
|
36
|
+
} catch {
|
|
37
|
+
return raw;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default function OrchestrationRunsPage() {
|
|
42
|
+
const [sessionKey, setSessionKey] = useState('');
|
|
43
|
+
const [runs, setRuns] = useState<RunListItem[]>([]);
|
|
44
|
+
const [selected, setSelected] = useState<RunDetail | null>(null);
|
|
45
|
+
const [loading, setLoading] = useState(false);
|
|
46
|
+
const [error, setError] = useState('');
|
|
47
|
+
|
|
48
|
+
const loadRuns = useCallback(async () => {
|
|
49
|
+
if (!sessionKey.trim()) {
|
|
50
|
+
setError('请输入 sessionKey');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
setLoading(true);
|
|
54
|
+
setError('');
|
|
55
|
+
setSelected(null);
|
|
56
|
+
try {
|
|
57
|
+
const res = await apiFetch(
|
|
58
|
+
`/api/agent/orchestration/runs?sessionKey=${encodeURIComponent(sessionKey.trim())}`,
|
|
59
|
+
);
|
|
60
|
+
const body = await res.json() as { success?: boolean; error?: string; data?: { runs?: RunListItem[] } };
|
|
61
|
+
if (!res.ok || !body.success) {
|
|
62
|
+
throw new Error(body.error ?? `HTTP ${res.status}`);
|
|
63
|
+
}
|
|
64
|
+
setRuns(body.data?.runs ?? []);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
67
|
+
setRuns([]);
|
|
68
|
+
} finally {
|
|
69
|
+
setLoading(false);
|
|
70
|
+
}
|
|
71
|
+
}, [sessionKey]);
|
|
72
|
+
|
|
73
|
+
const loadRunDetail = useCallback(async (runId: string) => {
|
|
74
|
+
setLoading(true);
|
|
75
|
+
setError('');
|
|
76
|
+
try {
|
|
77
|
+
const res = await apiFetch(`/api/agent/orchestration/runs/${encodeURIComponent(runId)}`);
|
|
78
|
+
const body = await res.json() as { success?: boolean; error?: string; data?: RunDetail };
|
|
79
|
+
if (!res.ok || !body.success || !body.data) {
|
|
80
|
+
throw new Error(body.error ?? `HTTP ${res.status}`);
|
|
81
|
+
}
|
|
82
|
+
setSelected(body.data);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
85
|
+
} finally {
|
|
86
|
+
setLoading(false);
|
|
87
|
+
}
|
|
88
|
+
}, []);
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div className="p-4 space-y-4 max-w-5xl">
|
|
92
|
+
<h1 className="text-lg font-semibold">编排 Run / Task / Event</h1>
|
|
93
|
+
<p className="text-sm text-muted-foreground">
|
|
94
|
+
从 OrchestrationKernel 投影(`GET /api/agent/orchestration/*`)。sessionKey 格式:
|
|
95
|
+
<code className="mx-1">platform:endpointId:kind:sceneId</code>
|
|
96
|
+
</p>
|
|
97
|
+
|
|
98
|
+
<div className="flex flex-wrap gap-2 items-end">
|
|
99
|
+
<label className="flex flex-col gap-1 text-sm flex-1 min-w-[240px]">
|
|
100
|
+
sessionKey
|
|
101
|
+
<input
|
|
102
|
+
className="border rounded px-2 py-1 text-sm bg-background"
|
|
103
|
+
value={sessionKey}
|
|
104
|
+
onChange={(e) => setSessionKey(e.target.value)}
|
|
105
|
+
placeholder="sandbox:solo-bot:private:user1"
|
|
106
|
+
/>
|
|
107
|
+
</label>
|
|
108
|
+
<button
|
|
109
|
+
type="button"
|
|
110
|
+
className="border rounded px-3 py-1 text-sm hover:bg-muted"
|
|
111
|
+
disabled={loading}
|
|
112
|
+
onClick={() => void loadRuns()}
|
|
113
|
+
>
|
|
114
|
+
查询 Runs
|
|
115
|
+
</button>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
{error ? <p className="text-sm text-red-600">{error}</p> : null}
|
|
119
|
+
|
|
120
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
121
|
+
<section className="border rounded p-3 space-y-2">
|
|
122
|
+
<h2 className="text-sm font-medium">Runs ({runs.length})</h2>
|
|
123
|
+
{runs.length === 0 ? (
|
|
124
|
+
<p className="text-sm text-muted-foreground">无数据</p>
|
|
125
|
+
) : (
|
|
126
|
+
<ul className="space-y-2 text-sm">
|
|
127
|
+
{runs.map((item) => (
|
|
128
|
+
<li key={item.run.id} className="border rounded p-2">
|
|
129
|
+
<button
|
|
130
|
+
type="button"
|
|
131
|
+
className="text-left w-full hover:underline"
|
|
132
|
+
onClick={() => void loadRunDetail(item.run.id)}
|
|
133
|
+
>
|
|
134
|
+
<div className="font-mono text-xs">{item.run.id}</div>
|
|
135
|
+
<div>{item.run.title || '(无标题)'}</div>
|
|
136
|
+
<div className="text-muted-foreground">
|
|
137
|
+
{item.run.status}
|
|
138
|
+
{' · '}
|
|
139
|
+
{item.tasks.length}
|
|
140
|
+
{' tasks'}
|
|
141
|
+
</div>
|
|
142
|
+
</button>
|
|
143
|
+
</li>
|
|
144
|
+
))}
|
|
145
|
+
</ul>
|
|
146
|
+
)}
|
|
147
|
+
</section>
|
|
148
|
+
|
|
149
|
+
<section className="border rounded p-3 space-y-3">
|
|
150
|
+
<h2 className="text-sm font-medium">Run 详情</h2>
|
|
151
|
+
{!selected ? (
|
|
152
|
+
<p className="text-sm text-muted-foreground">选择左侧 Run</p>
|
|
153
|
+
) : (
|
|
154
|
+
<>
|
|
155
|
+
<div className="text-sm space-y-1">
|
|
156
|
+
<div>
|
|
157
|
+
<span className="text-muted-foreground">ID </span>
|
|
158
|
+
<code>{selected.run.id}</code>
|
|
159
|
+
</div>
|
|
160
|
+
<div>
|
|
161
|
+
<span className="text-muted-foreground">状态 </span>
|
|
162
|
+
{selected.run.status}
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<div>
|
|
167
|
+
<h3 className="text-xs font-medium uppercase text-muted-foreground mb-1">Tasks</h3>
|
|
168
|
+
<ul className="space-y-2 text-sm">
|
|
169
|
+
{selected.tasks.map((task) => (
|
|
170
|
+
<li key={task.id} className="border rounded p-2 font-mono text-xs">
|
|
171
|
+
<div>
|
|
172
|
+
{task.id}
|
|
173
|
+
{' · '}
|
|
174
|
+
{task.status}
|
|
175
|
+
{' · '}
|
|
176
|
+
{task.executor_kind ?? 'local'}
|
|
177
|
+
</div>
|
|
178
|
+
<div>{task.name}</div>
|
|
179
|
+
{task.result_summary ? (
|
|
180
|
+
<div className="mt-1 whitespace-pre-wrap break-words">{task.result_summary}</div>
|
|
181
|
+
) : null}
|
|
182
|
+
{task.error ? (
|
|
183
|
+
<div className="mt-1 text-red-600 whitespace-pre-wrap">{task.error}</div>
|
|
184
|
+
) : null}
|
|
185
|
+
</li>
|
|
186
|
+
))}
|
|
187
|
+
</ul>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<div>
|
|
191
|
+
<h3 className="text-xs font-medium uppercase text-muted-foreground mb-1">Events</h3>
|
|
192
|
+
<ul className="space-y-2 text-xs max-h-64 overflow-auto">
|
|
193
|
+
{(selected.events ?? []).map((ev) => (
|
|
194
|
+
<li key={ev.seq} className="border rounded p-2">
|
|
195
|
+
<div>
|
|
196
|
+
#
|
|
197
|
+
{ev.seq}
|
|
198
|
+
{' '}
|
|
199
|
+
{ev.type}
|
|
200
|
+
{ev.task_id ? ` · task ${ev.task_id}` : ''}
|
|
201
|
+
</div>
|
|
202
|
+
{ev.payload_json ? (
|
|
203
|
+
<pre className="mt-1 whitespace-pre-wrap break-words text-[10px] opacity-80">
|
|
204
|
+
{parsePayload(ev.payload_json)}
|
|
205
|
+
</pre>
|
|
206
|
+
) : null}
|
|
207
|
+
</li>
|
|
208
|
+
))}
|
|
209
|
+
</ul>
|
|
210
|
+
</div>
|
|
211
|
+
</>
|
|
212
|
+
)}
|
|
213
|
+
</section>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { registerOrchestrationConsole } from './registerOrchestrationConsole.js';
|
|
3
|
+
|
|
4
|
+
describe('registerOrchestrationConsole', () => {
|
|
5
|
+
it('registers orchestration route and tool', () => {
|
|
6
|
+
const addRoute = vi.fn();
|
|
7
|
+
const addTool = vi.fn();
|
|
8
|
+
const React = { createElement: vi.fn((type) => ({ type })) };
|
|
9
|
+
|
|
10
|
+
registerOrchestrationConsole({
|
|
11
|
+
React: React as never,
|
|
12
|
+
addRoute,
|
|
13
|
+
addPage: addRoute,
|
|
14
|
+
addTool,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
expect(addRoute).toHaveBeenCalledWith(expect.objectContaining({
|
|
18
|
+
path: '/console/orchestration',
|
|
19
|
+
name: '编排',
|
|
20
|
+
}));
|
|
21
|
+
expect(addTool).toHaveBeenCalledWith(expect.objectContaining({
|
|
22
|
+
id: 'orchestration',
|
|
23
|
+
path: '/console/orchestration',
|
|
24
|
+
}));
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PluginRegisterHostApi } from '@zhin.js/contract';
|
|
2
|
+
import OrchestrationRunsPage from './OrchestrationRunsPage.js';
|
|
3
|
+
|
|
4
|
+
/** Remote Console v1.1 — OrchestrationKernel Run/Task/Event 投影页 */
|
|
5
|
+
export function registerOrchestrationConsole(api: PluginRegisterHostApi): void {
|
|
6
|
+
api.addRoute({
|
|
7
|
+
path: '/console/orchestration',
|
|
8
|
+
name: '编排',
|
|
9
|
+
element: api.React.createElement(OrchestrationRunsPage),
|
|
10
|
+
meta: { order: 40, group: 'Agent' },
|
|
11
|
+
});
|
|
12
|
+
api.addTool({
|
|
13
|
+
id: 'orchestration',
|
|
14
|
+
name: '编排',
|
|
15
|
+
path: '/console/orchestration',
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
const DB_NAME = "zhin-console";
|
|
2
|
-
const DB_VERSION =
|
|
2
|
+
const DB_VERSION = 2;
|
|
3
3
|
const STORE_INBOX = "inbox";
|
|
4
4
|
const STORE_PENDING = "pending";
|
|
5
5
|
|
|
6
6
|
export type InboxRecord = {
|
|
7
7
|
id: string;
|
|
8
8
|
adapter: string;
|
|
9
|
-
|
|
9
|
+
endpoint_id: string;
|
|
10
10
|
kind: "message" | "request" | "notice";
|
|
11
11
|
payload: unknown;
|
|
12
12
|
updatedAt: number;
|
|
@@ -42,7 +42,7 @@ export async function idbPutInbox(record: InboxRecord): Promise<void> {
|
|
|
42
42
|
|
|
43
43
|
export async function idbListInbox(
|
|
44
44
|
adapter: string,
|
|
45
|
-
|
|
45
|
+
endpoint_id: string,
|
|
46
46
|
kind: InboxRecord["kind"],
|
|
47
47
|
): Promise<InboxRecord[]> {
|
|
48
48
|
const db = await openDb();
|
|
@@ -53,22 +53,28 @@ export async function idbListInbox(
|
|
|
53
53
|
req.onerror = () => reject(req.error);
|
|
54
54
|
});
|
|
55
55
|
db.close();
|
|
56
|
-
return all.filter(
|
|
56
|
+
return all.filter(
|
|
57
|
+
(r) =>
|
|
58
|
+
r.adapter === adapter &&
|
|
59
|
+
(r.endpoint_id === endpoint_id ||
|
|
60
|
+
(r as { endpointId?: string }).endpointId === endpoint_id) &&
|
|
61
|
+
r.kind === kind,
|
|
62
|
+
);
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
export async function applyConsoleEvent(event: { type: string; data?: unknown }): Promise<void> {
|
|
60
66
|
const t = event.type;
|
|
61
|
-
if (t === "
|
|
67
|
+
if (t === "message.receive" || t === "request.receive" || t === "notice.receive") {
|
|
62
68
|
const data = event.data as Record<string, unknown> | undefined;
|
|
63
69
|
if (!data) return;
|
|
64
|
-
const adapter = String(data.adapter ?? "");
|
|
65
|
-
const
|
|
66
|
-
const id = `${adapter}:${
|
|
70
|
+
const adapter = String(data.$adapter ?? data.adapter ?? "");
|
|
71
|
+
const endpoint_id = String(data.$endpoint ?? data.endpoint_id ?? data.endpointId ?? data.bot ?? "");
|
|
72
|
+
const id = `${adapter}:${endpoint_id}:${t}:${Date.now()}`;
|
|
67
73
|
await idbPutInbox({
|
|
68
74
|
id,
|
|
69
75
|
adapter,
|
|
70
|
-
|
|
71
|
-
kind: t === "
|
|
76
|
+
endpoint_id,
|
|
77
|
+
kind: t === "message.receive" ? "message" : t === "request.receive" ? "request" : "notice",
|
|
72
78
|
payload: data,
|
|
73
79
|
updatedAt: Date.now(),
|
|
74
80
|
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { segmentsForAgentPanel, segmentsForImDelivery } from '../client/segments.js';
|
|
3
|
+
|
|
4
|
+
describe('console segmentsForImDelivery', () => {
|
|
5
|
+
it('inbox view strips AI-only segments', () => {
|
|
6
|
+
const full = [
|
|
7
|
+
{ type: 'thinking', data: { text: 'plan' } },
|
|
8
|
+
{ type: 'text', data: { text: 'hi' } },
|
|
9
|
+
{ type: 'tool_call', data: { name: 'bash' } },
|
|
10
|
+
];
|
|
11
|
+
expect(segmentsForImDelivery(full)).toEqual([
|
|
12
|
+
{ type: 'text', data: { text: 'hi' } },
|
|
13
|
+
]);
|
|
14
|
+
expect(segmentsForAgentPanel(full)).toEqual(full);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { MessageSegment } from './types.js';
|
|
2
|
+
|
|
3
|
+
/** IM 可见段白名单(与 core segment-contract/delivery 对齐) */
|
|
4
|
+
const IM_VISIBLE_TYPES = new Set([
|
|
5
|
+
'text', 'mention', 'image', 'video', 'audio', 'voice', 'record', 'file',
|
|
6
|
+
'face', 'reply', 'forward', 'link', 'dice', 'rps', 'markdown', 'html',
|
|
7
|
+
'qrcode', 'tts', 'keyboard', 'action',
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
const AI_ONLY_TYPES = new Set(['thinking', 'tool_call']);
|
|
11
|
+
|
|
12
|
+
function isSegmentLike(value: unknown): value is MessageSegment {
|
|
13
|
+
return (
|
|
14
|
+
typeof value === 'object'
|
|
15
|
+
&& value !== null
|
|
16
|
+
&& typeof (value as MessageSegment).type === 'string'
|
|
17
|
+
&& 'data' in value
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Inbox / 用户 IM 视图:仅展示 IM-visible 段(剔除 thinking、tool_call)。
|
|
23
|
+
* Agent 面板应使用全量 segments,勿调用此函数。
|
|
24
|
+
*/
|
|
25
|
+
export function segmentsForImDelivery(segments: readonly unknown[]): MessageSegment[] {
|
|
26
|
+
const out: MessageSegment[] = [];
|
|
27
|
+
for (const item of segments) {
|
|
28
|
+
if (!isSegmentLike(item)) continue;
|
|
29
|
+
if (AI_ONLY_TYPES.has(item.type)) continue;
|
|
30
|
+
if (!IM_VISIBLE_TYPES.has(item.type)) continue;
|
|
31
|
+
out.push(item);
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Agent 面板全量 segments(identity,便于双视图 API 对称) */
|
|
37
|
+
export function segmentsForAgentPanel(segments: readonly MessageSegment[]): MessageSegment[] {
|
|
38
|
+
return [...segments];
|
|
39
|
+
}
|
package/client/types.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
// 消息段类型定义 -
|
|
1
|
+
// 消息段类型定义 - 客户端版本(对齐 core Segment SSOT)
|
|
2
2
|
export interface MessageSegment {
|
|
3
|
-
/** record:如 ICQQ
|
|
4
|
-
type: 'text' | '
|
|
5
|
-
data: Record<string,
|
|
3
|
+
/** record:如 ICQQ 语音等,展示时按音频处理;thinking/tool_call 仅 agent 面板展示 */
|
|
4
|
+
type: 'text' | 'mention' | 'at' | 'image' | 'face' | 'video' | 'audio' | 'record' | 'file' | 'reply' | 'forward' | 'keyboard' | 'action' | 'thinking' | 'tool_call' | string
|
|
5
|
+
data: Record<string, unknown>
|
|
6
|
+
platform?: Record<string, unknown>
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
// 消息元素别名(兼容不同命名习惯)
|
|
@@ -10,4 +11,3 @@ export type MessageElement = MessageSegment
|
|
|
10
11
|
|
|
11
12
|
// 发送内容类型
|
|
12
13
|
export type SendContent = MessageSegment | MessageSegment[] | string
|
|
13
|
-
|
|
@@ -1,15 +1,5 @@
|
|
|
1
|
-
import type
|
|
2
|
-
|
|
3
|
-
WebSocketConfig,
|
|
4
|
-
WebSocketCallbacks,
|
|
5
|
-
} from "./types";
|
|
6
|
-
import {
|
|
7
|
-
ConnectionState,
|
|
8
|
-
WebSocketError,
|
|
9
|
-
ConnectionError,
|
|
10
|
-
MessageError,
|
|
11
|
-
RequestTimeoutError,
|
|
12
|
-
} from "./types";
|
|
1
|
+
import { ConnectionState, WebSocketError, ConnectionError, MessageError, RequestTimeoutError, type WebSocketMessage, type WebSocketConfig, type WebSocketCallbacks } from './types';
|
|
2
|
+
|
|
13
3
|
import { getApiBase, getStoredToken, resolveApiUrl } from "./remote-settings.js";
|
|
14
4
|
import { applyConsoleEvent } from "../persistence/idb-store.js";
|
|
15
5
|
|
|
@@ -350,7 +340,7 @@ export class WebSocketManager {
|
|
|
350
340
|
|
|
351
341
|
private handleBroadcast(message: WebSocketMessage): void {
|
|
352
342
|
const t = message.type;
|
|
353
|
-
if (t === "
|
|
343
|
+
if (t === "request.receive" || t === "notice.receive" || t === "message.receive") {
|
|
354
344
|
if (typeof window !== "undefined") {
|
|
355
345
|
window.dispatchEvent(new CustomEvent("zhin-console-bot-push", { detail: message }));
|
|
356
346
|
}
|
|
@@ -3,7 +3,7 @@ const TOKEN_KEY = "zhin_api_token";
|
|
|
3
3
|
|
|
4
4
|
export function getApiBase(): string {
|
|
5
5
|
const stored = localStorage.getItem(API_BASE_KEY)?.trim();
|
|
6
|
-
if (stored) return stored
|
|
6
|
+
if (stored) return trimTrailingSlash(stored);
|
|
7
7
|
if (typeof window !== "undefined") {
|
|
8
8
|
return window.location.origin;
|
|
9
9
|
}
|
|
@@ -11,11 +11,11 @@ export function getApiBase(): string {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export function setApiBase(base: string): void {
|
|
14
|
-
localStorage.setItem(API_BASE_KEY, base
|
|
14
|
+
localStorage.setItem(API_BASE_KEY, trimTrailingSlash(base));
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export function getStoredToken(): string | null {
|
|
18
|
-
return localStorage.getItem(TOKEN_KEY);
|
|
18
|
+
return getRuntimeToken() ?? localStorage.getItem(TOKEN_KEY);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export function resolveApiUrl(path: string): string {
|
|
@@ -23,3 +23,13 @@ export function resolveApiUrl(path: string): string {
|
|
|
23
23
|
const p = path.startsWith("/") ? path : `/${path}`;
|
|
24
24
|
return `${base}${p}`;
|
|
25
25
|
}
|
|
26
|
+
|
|
27
|
+
function trimTrailingSlash(value: string): string {
|
|
28
|
+
return value.endsWith("/") ? value.slice(0, -1) : value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getRuntimeToken(): string | null {
|
|
32
|
+
if (typeof window === "undefined") return null;
|
|
33
|
+
const token = (window as unknown as { __ZHIN_API_TOKEN?: string }).__ZHIN_API_TOKEN;
|
|
34
|
+
return token?.trim() || null;
|
|
35
|
+
}
|