@cniot/mdd-editor 0.3.0 → 0.3.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/README.MD +9 -0
- package/build/index.cjs.js +34 -18
- package/build/index.es.js +324 -10
- package/build/style.css +1 -1
- package/package.json +1 -1
- package/src/ai/LocalAIDrawer.jsx +210 -4
- package/src/ai/bridgeClient.js +145 -1
package/src/ai/LocalAIDrawer.jsx
CHANGED
|
@@ -8,10 +8,15 @@ import {
|
|
|
8
8
|
openPageWorkspace,
|
|
9
9
|
pullPage,
|
|
10
10
|
pushPage,
|
|
11
|
+
shouldUseBridgeRelay,
|
|
11
12
|
} from './bridgeClient';
|
|
12
13
|
import { buildPageIR, getPageCode } from './pageIR';
|
|
13
14
|
import { EVENT_KEY } from '$src/common/const';
|
|
14
15
|
|
|
16
|
+
const MIN_BRIDGE_VERSION = '0.1.4';
|
|
17
|
+
const START_COMMAND = `npm i -g @cniot/mdd-ai-bridge
|
|
18
|
+
mdd-ai-bridge`;
|
|
19
|
+
|
|
15
20
|
const parseSchema = (value) => {
|
|
16
21
|
if (!value) return null;
|
|
17
22
|
if (typeof value === 'string') {
|
|
@@ -33,14 +38,134 @@ const applySchemaToEditor = (schema, schemaJson) => {
|
|
|
33
38
|
schema.emit(EVENT_KEY.SCHEMA_UPDATE_FORCE, schemaJson);
|
|
34
39
|
};
|
|
35
40
|
|
|
41
|
+
const compareVersion = (a = '', b = '') => {
|
|
42
|
+
const left = String(a).split('.').map((item) => Number(item) || 0);
|
|
43
|
+
const right = String(b).split('.').map((item) => Number(item) || 0);
|
|
44
|
+
const max = Math.max(left.length, right.length);
|
|
45
|
+
for (let i = 0; i < max; i += 1) {
|
|
46
|
+
if ((left[i] || 0) > (right[i] || 0)) return 1;
|
|
47
|
+
if ((left[i] || 0) < (right[i] || 0)) return -1;
|
|
48
|
+
}
|
|
49
|
+
return 0;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const isBridgeVersionReady = (version) => Boolean(version) && compareVersion(version, MIN_BRIDGE_VERSION) >= 0;
|
|
53
|
+
|
|
54
|
+
const copyText = async (text) => {
|
|
55
|
+
if (typeof navigator !== 'undefined' && navigator?.clipboard?.writeText) {
|
|
56
|
+
await navigator.clipboard.writeText(text);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const textarea = document.createElement('textarea');
|
|
60
|
+
textarea.value = text;
|
|
61
|
+
textarea.setAttribute('readonly', 'readonly');
|
|
62
|
+
textarea.style.position = 'fixed';
|
|
63
|
+
textarea.style.left = '-9999px';
|
|
64
|
+
document.body.appendChild(textarea);
|
|
65
|
+
textarea.select();
|
|
66
|
+
document.execCommand('copy');
|
|
67
|
+
document.body.removeChild(textarea);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const getJsonErrorDetail = (error) => {
|
|
71
|
+
const jsonError = error?.data?.jsonError;
|
|
72
|
+
if (jsonError) {
|
|
73
|
+
return {
|
|
74
|
+
title: `${jsonError.file || 'JSON'} 解析失败${
|
|
75
|
+
jsonError.line ? `:第 ${jsonError.line} 行${jsonError.column ? `,第 ${jsonError.column} 列` : ''}` : ''
|
|
76
|
+
}`,
|
|
77
|
+
message: jsonError.message || error.message || '',
|
|
78
|
+
snippet: jsonError.snippet || '',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
title: '同步失败',
|
|
83
|
+
message: error?.message || '同步本地 AI 修改失败',
|
|
84
|
+
snippet: '',
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const getChangeTypeText = (type) => {
|
|
89
|
+
switch (type) {
|
|
90
|
+
case 'added':
|
|
91
|
+
return '新增';
|
|
92
|
+
case 'removed':
|
|
93
|
+
return '删除';
|
|
94
|
+
case 'type':
|
|
95
|
+
return '类型变化';
|
|
96
|
+
case 'changed':
|
|
97
|
+
return '修改';
|
|
98
|
+
default:
|
|
99
|
+
return type || '修改';
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
function PullDiffDetail({ diffSummary }) {
|
|
104
|
+
if (!diffSummary?.hasBaseline) return null;
|
|
105
|
+
const schemaChanges = diffSummary.schema?.changes || diffSummary.schema?.paths || [];
|
|
106
|
+
const diffText = diffSummary.diffText || '';
|
|
107
|
+
const hasDetail = schemaChanges.length > 0 || diffSummary.script?.changed || diffSummary.style?.changed || diffText;
|
|
108
|
+
if (!hasDetail) return null;
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<details className="mdd-local-ai-diff">
|
|
112
|
+
<summary>查看具体改动与 diff</summary>
|
|
113
|
+
<div className="mdd-local-ai-diff-content">
|
|
114
|
+
{schemaChanges.length > 0 ? (
|
|
115
|
+
<div className="mdd-local-ai-diff-section">
|
|
116
|
+
<div className="mdd-local-ai-diff-title">Schema 改动位置</div>
|
|
117
|
+
<ul className="mdd-local-ai-diff-list">
|
|
118
|
+
{schemaChanges.map((item, index) => {
|
|
119
|
+
const path = typeof item === 'string' ? item : item.path;
|
|
120
|
+
const type = typeof item === 'string' ? 'changed' : item.type;
|
|
121
|
+
return (
|
|
122
|
+
<li key={`${path}-${index}`}>
|
|
123
|
+
<code>{path}</code>
|
|
124
|
+
<span>{getChangeTypeText(type)}</span>
|
|
125
|
+
</li>
|
|
126
|
+
);
|
|
127
|
+
})}
|
|
128
|
+
</ul>
|
|
129
|
+
</div>
|
|
130
|
+
) : null}
|
|
131
|
+
|
|
132
|
+
{diffSummary.script?.changed ? (
|
|
133
|
+
<div className="mdd-local-ai-diff-section">
|
|
134
|
+
<div className="mdd-local-ai-diff-title">Script 改动</div>
|
|
135
|
+
<div>{diffSummary.script.message}</div>
|
|
136
|
+
{diffSummary.script.addedFunctions?.length ? (
|
|
137
|
+
<div>新增:{diffSummary.script.addedFunctions.join('、')}</div>
|
|
138
|
+
) : null}
|
|
139
|
+
{diffSummary.script.removedFunctions?.length ? (
|
|
140
|
+
<div>移除:{diffSummary.script.removedFunctions.join('、')}</div>
|
|
141
|
+
) : null}
|
|
142
|
+
</div>
|
|
143
|
+
) : null}
|
|
144
|
+
|
|
145
|
+
{diffSummary.style?.changed ? (
|
|
146
|
+
<div className="mdd-local-ai-diff-section">
|
|
147
|
+
<div className="mdd-local-ai-diff-title">Style 改动</div>
|
|
148
|
+
<div>{diffSummary.style.message}</div>
|
|
149
|
+
</div>
|
|
150
|
+
) : null}
|
|
151
|
+
|
|
152
|
+
{diffText ? <pre className="mdd-local-ai-diff-pre">{diffText}</pre> : null}
|
|
153
|
+
</div>
|
|
154
|
+
</details>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
36
158
|
export default function LocalAIDrawer(props) {
|
|
37
159
|
const { schema, scriptInfo, pageMeta = {}, bridgeConfig, onApply } = props;
|
|
38
160
|
const config = normalizeBridgeConfig(bridgeConfig);
|
|
39
161
|
const [baseURL, setBaseURL] = React.useState(config.baseURL || DEFAULT_BRIDGE_URL);
|
|
40
162
|
const [status, setStatus] = React.useState('未连接');
|
|
41
163
|
const [workspacePath, setWorkspacePath] = React.useState('');
|
|
164
|
+
const [bridgeInfo, setBridgeInfo] = React.useState(null);
|
|
42
165
|
const [loadingAction, setLoadingAction] = React.useState('');
|
|
43
166
|
const [lastSummary, setLastSummary] = React.useState('');
|
|
167
|
+
const [pullError, setPullError] = React.useState(null);
|
|
168
|
+
const [lastDiffSummary, setLastDiffSummary] = React.useState(null);
|
|
44
169
|
|
|
45
170
|
const schemaJson = schema?.getAllJSON?.() || {};
|
|
46
171
|
const pageCode = getPageCode(schemaJson, pageMeta);
|
|
@@ -68,13 +193,54 @@ export default function LocalAIDrawer(props) {
|
|
|
68
193
|
};
|
|
69
194
|
}, [pageCode, pageMeta, schema, scriptInfo]);
|
|
70
195
|
|
|
196
|
+
const expectedWorkspacePath = React.useMemo(() => {
|
|
197
|
+
if (workspacePath) return workspacePath;
|
|
198
|
+
const root = bridgeInfo?.workspaceRoot || '~/.mdd-ai-workspace';
|
|
199
|
+
return `${root.replace(/\/$/, '')}/pages/${pageCode}`;
|
|
200
|
+
}, [bridgeInfo, pageCode, workspacePath]);
|
|
201
|
+
|
|
202
|
+
const aiPrompt = React.useMemo(
|
|
203
|
+
() =>
|
|
204
|
+
`请帮我修改这个 MDD 页面:
|
|
205
|
+
|
|
206
|
+
工作区:${expectedWorkspacePath}
|
|
207
|
+
页面 Code:${pageCode}
|
|
208
|
+
|
|
209
|
+
请先阅读同目录下的 AGENTS.md 和 PROMPT.md,再按需求修改:
|
|
210
|
+
- mdd.schema.json:页面 schema
|
|
211
|
+
- mdd.script.jsx:页面脚本
|
|
212
|
+
- mdd.style.less:页面样式
|
|
213
|
+
|
|
214
|
+
注意:
|
|
215
|
+
1. 不要修改 page.ir.json / page.meta.json 作为最终结果。
|
|
216
|
+
2. JSON 必须保持合法,尽量只改和需求相关的位置。
|
|
217
|
+
3. MDD hook / engine API 不确定时,先读 context 目录里的 api-cheatsheet.md、engine-runtime.md、hooks-cookbook.md。
|
|
218
|
+
|
|
219
|
+
我的需求是:`,
|
|
220
|
+
[expectedWorkspacePath, pageCode],
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
const handleCopy = React.useCallback(async (text, successText) => {
|
|
224
|
+
try {
|
|
225
|
+
await copyText(text);
|
|
226
|
+
Message.success(successText);
|
|
227
|
+
} catch (e) {
|
|
228
|
+
Message.error('复制失败,请手动选择复制');
|
|
229
|
+
}
|
|
230
|
+
}, []);
|
|
231
|
+
|
|
71
232
|
const checkHealth = React.useCallback(async () => {
|
|
72
233
|
setLoadingAction('health');
|
|
73
234
|
try {
|
|
74
235
|
const res = await health(baseURL);
|
|
75
|
-
|
|
76
|
-
if (res?.
|
|
77
|
-
|
|
236
|
+
setBridgeInfo(res || null);
|
|
237
|
+
if (res?.ok && isBridgeVersionReady(res.version)) {
|
|
238
|
+
setStatus(`已连接(Bridge v${res.version})`);
|
|
239
|
+
} else if (res?.ok) {
|
|
240
|
+
setStatus(`Bridge 版本偏旧${res?.version ? `(v${res.version})` : ''}`);
|
|
241
|
+
setLastSummary(`建议升级本地 bridge 到 ${MIN_BRIDGE_VERSION} 或更高版本:npm i -g @cniot/mdd-ai-bridge@latest`);
|
|
242
|
+
} else {
|
|
243
|
+
setStatus('连接异常');
|
|
78
244
|
}
|
|
79
245
|
} catch (e) {
|
|
80
246
|
setStatus('未连接');
|
|
@@ -86,9 +252,15 @@ export default function LocalAIDrawer(props) {
|
|
|
86
252
|
|
|
87
253
|
const handlePush = async () => {
|
|
88
254
|
setLoadingAction('push');
|
|
255
|
+
setPullError(null);
|
|
256
|
+
setLastDiffSummary(null);
|
|
89
257
|
try {
|
|
90
258
|
const res = await pushPage(baseURL, pageCode, getPayload());
|
|
91
259
|
setWorkspacePath(res?.dir || '');
|
|
260
|
+
setBridgeInfo((prev) => ({
|
|
261
|
+
...(prev || {}),
|
|
262
|
+
workspaceRoot: res?.context?.workspaceRoot || prev?.workspaceRoot,
|
|
263
|
+
}));
|
|
92
264
|
setStatus('已同步到本地');
|
|
93
265
|
setLastSummary(`已写入本地工作区: ${res?.dir || pageCode}`);
|
|
94
266
|
Message.success('已发送到本地 AI 工作区');
|
|
@@ -101,6 +273,7 @@ export default function LocalAIDrawer(props) {
|
|
|
101
273
|
|
|
102
274
|
const handlePull = async () => {
|
|
103
275
|
setLoadingAction('pull');
|
|
276
|
+
setPullError(null);
|
|
104
277
|
try {
|
|
105
278
|
const res = await pullPage(baseURL, pageCode);
|
|
106
279
|
const nextSchema = parseSchema(res?.schemaInfo);
|
|
@@ -116,9 +289,13 @@ export default function LocalAIDrawer(props) {
|
|
|
116
289
|
raw: res,
|
|
117
290
|
});
|
|
118
291
|
setLastSummary(res?.summary || '已从本地工作区同步修改');
|
|
292
|
+
setLastDiffSummary(res?.diffSummary || null);
|
|
119
293
|
Message.success('已同步本地 AI 修改');
|
|
120
294
|
} catch (e) {
|
|
121
|
-
|
|
295
|
+
const detail = getJsonErrorDetail(e);
|
|
296
|
+
setPullError(detail);
|
|
297
|
+
setLastDiffSummary(null);
|
|
298
|
+
Message.error(detail.title || '同步本地 AI 修改失败');
|
|
122
299
|
} finally {
|
|
123
300
|
setLoadingAction('');
|
|
124
301
|
}
|
|
@@ -129,6 +306,7 @@ export default function LocalAIDrawer(props) {
|
|
|
129
306
|
try {
|
|
130
307
|
const res = await getPageStatus(baseURL, pageCode);
|
|
131
308
|
setWorkspacePath(res?.dir || '');
|
|
309
|
+
setLastDiffSummary(null);
|
|
132
310
|
setLastSummary(res?.exists ? `本地工作区已存在: ${res.dir}` : '本地还没有这个页面的工作区');
|
|
133
311
|
Message.success(res?.exists ? '本地工作区已存在' : '本地工作区不存在');
|
|
134
312
|
} catch (e) {
|
|
@@ -152,6 +330,10 @@ export default function LocalAIDrawer(props) {
|
|
|
152
330
|
};
|
|
153
331
|
|
|
154
332
|
React.useEffect(() => {
|
|
333
|
+
if (shouldUseBridgeRelay(baseURL)) {
|
|
334
|
+
setStatus('点击按钮后通过本地 relay 连接');
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
155
337
|
checkHealth();
|
|
156
338
|
}, []);
|
|
157
339
|
|
|
@@ -173,6 +355,7 @@ export default function LocalAIDrawer(props) {
|
|
|
173
355
|
</div>
|
|
174
356
|
<div className="mdd-local-ai-meta">
|
|
175
357
|
<div>状态:{status}</div>
|
|
358
|
+
{bridgeInfo?.version ? <div>Bridge 版本:{bridgeInfo.version}</div> : null}
|
|
176
359
|
<div>页面:{pageCode}</div>
|
|
177
360
|
{workspacePath ? <div>目录:{workspacePath}</div> : null}
|
|
178
361
|
</div>
|
|
@@ -193,8 +376,30 @@ export default function LocalAIDrawer(props) {
|
|
|
193
376
|
</CnButton>
|
|
194
377
|
</div>
|
|
195
378
|
|
|
379
|
+
<div className="mdd-local-ai-copy-actions">
|
|
380
|
+
<CnButton size="small" onClick={() => handleCopy(START_COMMAND, '已复制启动命令')}>
|
|
381
|
+
复制启动命令
|
|
382
|
+
</CnButton>
|
|
383
|
+
<CnButton size="small" onClick={() => handleCopy(expectedWorkspacePath, '已复制工作区路径')}>
|
|
384
|
+
复制工作区路径
|
|
385
|
+
</CnButton>
|
|
386
|
+
<CnButton size="small" onClick={() => handleCopy(aiPrompt, '已复制 AI 提示词')}>
|
|
387
|
+
复制 AI 提示词
|
|
388
|
+
</CnButton>
|
|
389
|
+
</div>
|
|
390
|
+
|
|
196
391
|
{lastSummary ? <div className="mdd-local-ai-summary">{lastSummary}</div> : null}
|
|
197
392
|
|
|
393
|
+
<PullDiffDetail diffSummary={lastDiffSummary} />
|
|
394
|
+
|
|
395
|
+
{pullError ? (
|
|
396
|
+
<div className="mdd-local-ai-error">
|
|
397
|
+
<div className="mdd-local-ai-error-title">{pullError.title}</div>
|
|
398
|
+
<div className="mdd-local-ai-error-message">{pullError.message}</div>
|
|
399
|
+
{pullError.snippet ? <pre className="mdd-local-ai-error-snippet">{pullError.snippet}</pre> : null}
|
|
400
|
+
</div>
|
|
401
|
+
) : null}
|
|
402
|
+
|
|
198
403
|
<CnCard className="mdd-local-ai-card">
|
|
199
404
|
<div className="mdd-local-ai-title">使用方式</div>
|
|
200
405
|
<div className="mdd-local-ai-tips">
|
|
@@ -203,6 +408,7 @@ export default function LocalAIDrawer(props) {
|
|
|
203
408
|
<div>3. 点击“发送到本地 AI”,当前页面会写入本地工作区。</div>
|
|
204
409
|
<div>4. 用 Cursor、Qoder、Codex CLI 等工具打开目录并修改文件。</div>
|
|
205
410
|
<div>5. 点击“同步本地修改”,确认预览后继续使用原保存按钮。</div>
|
|
411
|
+
<div>线上 HTTP 页面会自动打开本地 Bridge relay 窗口,请允许浏览器弹窗。</div>
|
|
206
412
|
<div>本地工作区默认在 ~/.mdd-ai-workspace。</div>
|
|
207
413
|
</div>
|
|
208
414
|
</CnCard>
|
package/src/ai/bridgeClient.js
CHANGED
|
@@ -1,8 +1,33 @@
|
|
|
1
1
|
export const DEFAULT_BRIDGE_URL = 'http://127.0.0.1:17678';
|
|
2
2
|
|
|
3
3
|
const trimEndSlash = (value = '') => value.replace(/\/+$/, '');
|
|
4
|
+
const RELAY_CHANNEL = 'mdd-ai-bridge';
|
|
5
|
+
const relayCacheMap = new Map();
|
|
6
|
+
|
|
7
|
+
const isLoopbackURL = (value = '') => /^http:\/\/(127(?:\.\d{1,3}){3}|localhost)(?::\d+)?/i.test(value);
|
|
8
|
+
|
|
9
|
+
export class BridgeRequestError extends Error {
|
|
10
|
+
constructor(message, options = {}) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = 'BridgeRequestError';
|
|
13
|
+
this.status = options.status || 0;
|
|
14
|
+
this.data = options.data || null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function shouldUseBridgeRelay(baseURL) {
|
|
19
|
+
return typeof window !== 'undefined' && !window.isSecureContext && isLoopbackURL(baseURL);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function createRequestId() {
|
|
23
|
+
return `${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
24
|
+
}
|
|
4
25
|
|
|
5
26
|
async function request(baseURL, path, options = {}) {
|
|
27
|
+
if (shouldUseBridgeRelay(baseURL)) {
|
|
28
|
+
return requestViaRelay(baseURL, path, options);
|
|
29
|
+
}
|
|
30
|
+
|
|
6
31
|
const res = await fetch(`${trimEndSlash(baseURL)}${path}`, {
|
|
7
32
|
...options,
|
|
8
33
|
headers: {
|
|
@@ -20,11 +45,130 @@ async function request(baseURL, path, options = {}) {
|
|
|
20
45
|
}
|
|
21
46
|
|
|
22
47
|
if (!res.ok) {
|
|
23
|
-
throw new
|
|
48
|
+
throw new BridgeRequestError(data?.message || `请求本地 AI Bridge 失败: ${res.status}`, {
|
|
49
|
+
status: res.status,
|
|
50
|
+
data,
|
|
51
|
+
});
|
|
24
52
|
}
|
|
25
53
|
return data;
|
|
26
54
|
}
|
|
27
55
|
|
|
56
|
+
function getRelayCache(baseURL) {
|
|
57
|
+
const relayOrigin = trimEndSlash(baseURL);
|
|
58
|
+
const relayURL = `${relayOrigin}/relay`;
|
|
59
|
+
const cached = relayCacheMap.get(relayOrigin);
|
|
60
|
+
if (cached?.window && !cached.window.closed) {
|
|
61
|
+
return cached;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const relayWindow = window.open(
|
|
65
|
+
relayURL,
|
|
66
|
+
'mdd-ai-bridge-relay',
|
|
67
|
+
'width=520,height=320,menubar=no,toolbar=no,location=no,status=no',
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
if (!relayWindow) {
|
|
71
|
+
throw new Error('浏览器拦截了本地 Bridge 窗口,请允许弹窗后重试');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const next = {
|
|
75
|
+
origin: relayOrigin,
|
|
76
|
+
window: relayWindow,
|
|
77
|
+
ready: false,
|
|
78
|
+
};
|
|
79
|
+
relayCacheMap.set(relayOrigin, next);
|
|
80
|
+
return next;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function requestViaRelay(baseURL, path, options = {}) {
|
|
84
|
+
const relayCache = getRelayCache(baseURL);
|
|
85
|
+
const requestId = createRequestId();
|
|
86
|
+
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
let settled = false;
|
|
89
|
+
let fallbackPosted = false;
|
|
90
|
+
let readyPosted = false;
|
|
91
|
+
|
|
92
|
+
const cleanup = () => {
|
|
93
|
+
window.removeEventListener('message', handleMessage);
|
|
94
|
+
clearTimeout(timeoutTimer);
|
|
95
|
+
clearTimeout(fallbackTimer);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const postRequest = (source = 'ready') => {
|
|
99
|
+
if (source === 'ready') {
|
|
100
|
+
if (readyPosted) return;
|
|
101
|
+
readyPosted = true;
|
|
102
|
+
} else {
|
|
103
|
+
if (fallbackPosted) return;
|
|
104
|
+
fallbackPosted = true;
|
|
105
|
+
}
|
|
106
|
+
if (!relayCache.window || relayCache.window.closed) {
|
|
107
|
+
finish(reject, new Error('本地 Bridge relay 窗口已关闭,请重试'));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
relayCache.window.postMessage(
|
|
111
|
+
{
|
|
112
|
+
channel: RELAY_CHANNEL,
|
|
113
|
+
type: 'request',
|
|
114
|
+
id: requestId,
|
|
115
|
+
path,
|
|
116
|
+
options,
|
|
117
|
+
},
|
|
118
|
+
relayCache.origin,
|
|
119
|
+
);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const finish = (callback, value) => {
|
|
123
|
+
if (settled) return;
|
|
124
|
+
settled = true;
|
|
125
|
+
cleanup();
|
|
126
|
+
callback(value);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const handleMessage = (event) => {
|
|
130
|
+
if (event.origin !== relayCache.origin) return;
|
|
131
|
+
if (event.source !== relayCache.window) return;
|
|
132
|
+
const message = event.data || {};
|
|
133
|
+
if (message.channel !== RELAY_CHANNEL) return;
|
|
134
|
+
|
|
135
|
+
if (message.type === 'ready') {
|
|
136
|
+
relayCache.ready = true;
|
|
137
|
+
postRequest('ready');
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (message.type !== 'response' || message.id !== requestId) return;
|
|
142
|
+
if (!message.ok) {
|
|
143
|
+
finish(
|
|
144
|
+
reject,
|
|
145
|
+
new BridgeRequestError(
|
|
146
|
+
message.data?.message || `请求本地 AI Bridge 失败: ${message.status || 0}`,
|
|
147
|
+
{
|
|
148
|
+
status: message.status || 0,
|
|
149
|
+
data: message.data,
|
|
150
|
+
},
|
|
151
|
+
),
|
|
152
|
+
);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
finish(resolve, message.data);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const timeoutTimer = setTimeout(() => {
|
|
159
|
+
finish(reject, new Error('本地 AI Bridge relay 响应超时,请确认服务已启动'));
|
|
160
|
+
}, 30000);
|
|
161
|
+
const fallbackTimer = setTimeout(() => {
|
|
162
|
+
if (!relayCache.ready) postRequest('fallback');
|
|
163
|
+
}, 500);
|
|
164
|
+
|
|
165
|
+
window.addEventListener('message', handleMessage);
|
|
166
|
+
if (relayCache.ready) {
|
|
167
|
+
setTimeout(() => postRequest('ready'), 0);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
28
172
|
export function normalizeBridgeConfig(config) {
|
|
29
173
|
if (config === false) {
|
|
30
174
|
return {
|