@leeoohoo/ui-apps-devkit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -0
- package/bin/chatos-uiapp.js +5 -0
- package/package.json +22 -0
- package/src/cli.js +53 -0
- package/src/commands/dev.js +14 -0
- package/src/commands/init.js +141 -0
- package/src/commands/install.js +55 -0
- package/src/commands/pack.js +72 -0
- package/src/commands/validate.js +103 -0
- package/src/lib/args.js +49 -0
- package/src/lib/config.js +29 -0
- package/src/lib/fs.js +78 -0
- package/src/lib/path-boundary.js +16 -0
- package/src/lib/plugin.js +45 -0
- package/src/lib/template.js +168 -0
- package/src/sandbox/server.js +861 -0
- package/templates/basic/README.md +58 -0
- package/templates/basic/chatos.config.json +5 -0
- package/templates/basic/docs/CHATOS_UI_APPS_AI_CONTRIBUTIONS.md +181 -0
- package/templates/basic/docs/CHATOS_UI_APPS_BACKEND_PROTOCOL.md +74 -0
- package/templates/basic/docs/CHATOS_UI_APPS_HOST_API.md +123 -0
- package/templates/basic/docs/CHATOS_UI_APPS_OVERVIEW.md +110 -0
- package/templates/basic/docs/CHATOS_UI_APPS_PLUGIN_MANIFEST.md +227 -0
- package/templates/basic/docs/CHATOS_UI_PROMPTS_PROTOCOL.md +392 -0
- package/templates/basic/plugin/apps/app/index.mjs +263 -0
- package/templates/basic/plugin/apps/app/mcp-prompt.en.md +7 -0
- package/templates/basic/plugin/apps/app/mcp-prompt.zh.md +7 -0
- package/templates/basic/plugin/apps/app/mcp-server.mjs +15 -0
- package/templates/basic/plugin/backend/index.mjs +37 -0
- package/templates/basic/template.json +7 -0
- package/templates/notepad/README.md +36 -0
- package/templates/notepad/chatos.config.json +4 -0
- package/templates/notepad/docs/CHATOS_UI_APPS_AI_CONTRIBUTIONS.md +181 -0
- package/templates/notepad/docs/CHATOS_UI_APPS_BACKEND_PROTOCOL.md +74 -0
- package/templates/notepad/docs/CHATOS_UI_APPS_HOST_API.md +123 -0
- package/templates/notepad/docs/CHATOS_UI_APPS_OVERVIEW.md +110 -0
- package/templates/notepad/docs/CHATOS_UI_APPS_PLUGIN_MANIFEST.md +227 -0
- package/templates/notepad/docs/CHATOS_UI_PROMPTS_PROTOCOL.md +392 -0
- package/templates/notepad/plugin/apps/app/api.mjs +30 -0
- package/templates/notepad/plugin/apps/app/dom.mjs +14 -0
- package/templates/notepad/plugin/apps/app/ds-tree.mjs +35 -0
- package/templates/notepad/plugin/apps/app/index.mjs +1056 -0
- package/templates/notepad/plugin/apps/app/layers.mjs +338 -0
- package/templates/notepad/plugin/apps/app/markdown.mjs +120 -0
- package/templates/notepad/plugin/apps/app/mcp-prompt.en.md +22 -0
- package/templates/notepad/plugin/apps/app/mcp-prompt.zh.md +22 -0
- package/templates/notepad/plugin/apps/app/mcp-server.mjs +200 -0
- package/templates/notepad/plugin/apps/app/styles.mjs +355 -0
- package/templates/notepad/plugin/apps/app/tags.mjs +21 -0
- package/templates/notepad/plugin/apps/app/ui.mjs +280 -0
- package/templates/notepad/plugin/backend/index.mjs +99 -0
- package/templates/notepad/plugin/plugin.json +23 -0
- package/templates/notepad/plugin/shared/notepad-paths.mjs +62 -0
- package/templates/notepad/plugin/shared/notepad-store.mjs +765 -0
- package/templates/notepad/template.json +8 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
export function mount({ container, host, slots }) {
|
|
2
|
+
if (!container) throw new Error('container is required');
|
|
3
|
+
if (!host || typeof host !== 'object') throw new Error('host is required');
|
|
4
|
+
|
|
5
|
+
const headerSlot =
|
|
6
|
+
slots?.header && typeof slots.header === 'object' && typeof slots.header.appendChild === 'function' ? slots.header : null;
|
|
7
|
+
|
|
8
|
+
const ctx = typeof host?.context?.get === 'function' ? host.context.get() : { pluginId: '', appId: '', theme: 'light' };
|
|
9
|
+
const bridgeEnabled = Boolean(ctx?.bridge?.enabled);
|
|
10
|
+
|
|
11
|
+
const root = document.createElement('div');
|
|
12
|
+
root.style.height = '100%';
|
|
13
|
+
root.style.boxSizing = 'border-box';
|
|
14
|
+
root.style.padding = '14px';
|
|
15
|
+
root.style.display = 'flex';
|
|
16
|
+
root.style.flexDirection = 'column';
|
|
17
|
+
root.style.gap = '12px';
|
|
18
|
+
|
|
19
|
+
const title = document.createElement('div');
|
|
20
|
+
title.textContent = '__PLUGIN_NAME__ · __APP_ID__';
|
|
21
|
+
title.style.fontWeight = '800';
|
|
22
|
+
|
|
23
|
+
const meta = document.createElement('div');
|
|
24
|
+
meta.style.fontSize = '12px';
|
|
25
|
+
meta.style.opacity = '0.75';
|
|
26
|
+
meta.textContent = `${ctx?.pluginId || ''}:${ctx?.appId || ''} · theme=${ctx?.theme || 'light'} · bridge=${bridgeEnabled ? 'enabled' : 'disabled'}`;
|
|
27
|
+
|
|
28
|
+
const header = document.createElement('div');
|
|
29
|
+
header.appendChild(title);
|
|
30
|
+
header.appendChild(meta);
|
|
31
|
+
|
|
32
|
+
const body = document.createElement('div');
|
|
33
|
+
body.style.display = 'grid';
|
|
34
|
+
body.style.gridTemplateColumns = '320px 1fr';
|
|
35
|
+
body.style.gap = '12px';
|
|
36
|
+
body.style.flex = '1';
|
|
37
|
+
body.style.minHeight = '0';
|
|
38
|
+
|
|
39
|
+
const actions = document.createElement('div');
|
|
40
|
+
actions.style.border = '1px solid rgba(0,0,0,0.12)';
|
|
41
|
+
actions.style.borderRadius = '14px';
|
|
42
|
+
actions.style.padding = '12px';
|
|
43
|
+
actions.style.display = 'grid';
|
|
44
|
+
actions.style.gap = '10px';
|
|
45
|
+
|
|
46
|
+
const input = document.createElement('textarea');
|
|
47
|
+
input.placeholder = '输入要发送给 ChatOS / 后端 LLM 的内容…';
|
|
48
|
+
input.style.width = '100%';
|
|
49
|
+
input.style.minHeight = '86px';
|
|
50
|
+
input.style.boxSizing = 'border-box';
|
|
51
|
+
input.style.borderRadius = '12px';
|
|
52
|
+
input.style.border = '1px solid rgba(0,0,0,0.14)';
|
|
53
|
+
input.style.background = 'rgba(0,0,0,0.04)';
|
|
54
|
+
input.style.padding = '10px 10px';
|
|
55
|
+
input.style.resize = 'vertical';
|
|
56
|
+
input.style.outline = 'none';
|
|
57
|
+
|
|
58
|
+
const log = document.createElement('pre');
|
|
59
|
+
log.style.border = '1px solid rgba(0,0,0,0.12)';
|
|
60
|
+
log.style.borderRadius = '14px';
|
|
61
|
+
log.style.padding = '12px';
|
|
62
|
+
log.style.margin = '0';
|
|
63
|
+
log.style.overflow = 'auto';
|
|
64
|
+
log.style.minHeight = '0';
|
|
65
|
+
|
|
66
|
+
const appendLog = (type, payload) => {
|
|
67
|
+
const ts = new Date().toISOString();
|
|
68
|
+
log.textContent += `[${ts}] ${type}${payload !== undefined ? ` ${JSON.stringify(payload, null, 2)}` : ''}\n`;
|
|
69
|
+
log.scrollTop = log.scrollHeight;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const mkBtn = (label) => {
|
|
73
|
+
const btn = document.createElement('button');
|
|
74
|
+
btn.type = 'button';
|
|
75
|
+
btn.textContent = label;
|
|
76
|
+
btn.style.padding = '9px 10px';
|
|
77
|
+
btn.style.borderRadius = '12px';
|
|
78
|
+
btn.style.border = '1px solid rgba(0,0,0,0.14)';
|
|
79
|
+
btn.style.background = 'rgba(0,0,0,0.04)';
|
|
80
|
+
btn.style.cursor = 'pointer';
|
|
81
|
+
btn.style.fontWeight = '650';
|
|
82
|
+
return btn;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const run = async (label, fn) => {
|
|
86
|
+
try {
|
|
87
|
+
const res = await fn();
|
|
88
|
+
appendLog(label, res);
|
|
89
|
+
return res;
|
|
90
|
+
} catch (e) {
|
|
91
|
+
appendLog(`${label}.error`, { message: e?.message || String(e) });
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const btnPing = mkBtn('backend.invoke("ping")');
|
|
97
|
+
btnPing.addEventListener('click', () => run('backend.invoke', () => host.backend.invoke('ping', { hello: 'world' })));
|
|
98
|
+
|
|
99
|
+
const btnLlmComplete = mkBtn('backend.invoke("llmComplete")');
|
|
100
|
+
btnLlmComplete.addEventListener('click', async () => {
|
|
101
|
+
const text = String(input.value || '').trim();
|
|
102
|
+
if (!text) return;
|
|
103
|
+
btnLlmComplete.disabled = true;
|
|
104
|
+
try {
|
|
105
|
+
const res = await run('backend.invoke.llmComplete', () => host.backend.invoke('llmComplete', { input: text }));
|
|
106
|
+
if (res?.content) {
|
|
107
|
+
log.textContent += `\n[sandbox llm]\n${String(res.content)}\n`;
|
|
108
|
+
log.scrollTop = log.scrollHeight;
|
|
109
|
+
}
|
|
110
|
+
} finally {
|
|
111
|
+
btnLlmComplete.disabled = false;
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const btnPrompt = mkBtn('uiPrompts.request(kv)');
|
|
116
|
+
btnPrompt.addEventListener('click', async () => {
|
|
117
|
+
try {
|
|
118
|
+
const res = await host.uiPrompts.request({
|
|
119
|
+
prompt: {
|
|
120
|
+
kind: 'kv',
|
|
121
|
+
title: '需要你补充信息',
|
|
122
|
+
message: '填写后点 Submit',
|
|
123
|
+
fields: [
|
|
124
|
+
{ key: 'name', label: '姓名', placeholder: '请输入', required: true },
|
|
125
|
+
{ key: 'note', label: '备注', placeholder: '可选', multiline: true },
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
appendLog('uiPrompts.request', res);
|
|
130
|
+
host.uiPrompts.open();
|
|
131
|
+
} catch (e) {
|
|
132
|
+
appendLog('uiPrompts.request.error', { message: e?.message || String(e) });
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
let activeSessionId = '';
|
|
137
|
+
let chatUnsub = null;
|
|
138
|
+
|
|
139
|
+
const ensureSession = async () => {
|
|
140
|
+
const agents = await run('chat.agents.list', () => host.chat.agents.list());
|
|
141
|
+
let agentId = agents?.agents?.[0]?.id || '';
|
|
142
|
+
if (!agentId) {
|
|
143
|
+
const ensured = await run('chat.agents.ensureDefault', () => host.chat.agents.ensureDefault());
|
|
144
|
+
agentId = ensured?.agent?.id || ensured?.agents?.[0]?.id || '';
|
|
145
|
+
}
|
|
146
|
+
if (!agentId) throw new Error('no agentId');
|
|
147
|
+
|
|
148
|
+
const res = await run('chat.sessions.ensureDefault', () => host.chat.sessions.ensureDefault({ agentId }));
|
|
149
|
+
activeSessionId = res?.session?.id || '';
|
|
150
|
+
if (activeSessionId) appendLog('activeSessionId', activeSessionId);
|
|
151
|
+
return activeSessionId;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const btnEnsureSession = mkBtn('chat.sessions.ensureDefault');
|
|
155
|
+
btnEnsureSession.addEventListener('click', () => ensureSession().catch((e) => appendLog('chat.ensureSession.error', { message: e?.message || String(e) })));
|
|
156
|
+
|
|
157
|
+
const btnSend = mkBtn('chat.send');
|
|
158
|
+
btnSend.addEventListener('click', async () => {
|
|
159
|
+
const text = String(input.value || '').trim();
|
|
160
|
+
if (!text) return;
|
|
161
|
+
btnSend.disabled = true;
|
|
162
|
+
try {
|
|
163
|
+
if (!activeSessionId) await ensureSession();
|
|
164
|
+
if (!activeSessionId) throw new Error('no sessionId');
|
|
165
|
+
await run('chat.send', () => host.chat.send({ sessionId: activeSessionId, text }));
|
|
166
|
+
input.value = '';
|
|
167
|
+
} finally {
|
|
168
|
+
btnSend.disabled = false;
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const btnSub = mkBtn('chat.events.subscribe');
|
|
173
|
+
btnSub.addEventListener('click', async () => {
|
|
174
|
+
if (!activeSessionId) await ensureSession();
|
|
175
|
+
if (!activeSessionId) {
|
|
176
|
+
appendLog('chat.events.subscribe.error', { message: 'no sessionId' });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (chatUnsub) {
|
|
180
|
+
try {
|
|
181
|
+
chatUnsub();
|
|
182
|
+
} catch {
|
|
183
|
+
// ignore
|
|
184
|
+
}
|
|
185
|
+
chatUnsub = null;
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
chatUnsub = host.chat.events.subscribe({ sessionId: activeSessionId }, (payload) => {
|
|
189
|
+
appendLog('chat.event', payload);
|
|
190
|
+
if (payload?.type === 'assistant_delta' && payload?.delta) {
|
|
191
|
+
log.textContent += payload.delta;
|
|
192
|
+
log.scrollTop = log.scrollHeight;
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
appendLog('chat.events.subscribe', { ok: true });
|
|
196
|
+
} catch (e) {
|
|
197
|
+
appendLog('chat.events.subscribe.error', { message: e?.message || String(e) });
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const btnUnsub = mkBtn('chat.events.unsubscribe');
|
|
202
|
+
btnUnsub.addEventListener('click', () => {
|
|
203
|
+
if (chatUnsub) {
|
|
204
|
+
try {
|
|
205
|
+
chatUnsub();
|
|
206
|
+
} catch {
|
|
207
|
+
// ignore
|
|
208
|
+
}
|
|
209
|
+
chatUnsub = null;
|
|
210
|
+
appendLog('chat.events.unsubscribe', { ok: true });
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
run('chat.events.unsubscribe', () => host.chat.events.unsubscribe());
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
actions.appendChild(btnPing);
|
|
217
|
+
actions.appendChild(btnLlmComplete);
|
|
218
|
+
actions.appendChild(btnPrompt);
|
|
219
|
+
actions.appendChild(input);
|
|
220
|
+
actions.appendChild(btnEnsureSession);
|
|
221
|
+
actions.appendChild(btnSend);
|
|
222
|
+
actions.appendChild(btnSub);
|
|
223
|
+
actions.appendChild(btnUnsub);
|
|
224
|
+
|
|
225
|
+
body.appendChild(actions);
|
|
226
|
+
body.appendChild(log);
|
|
227
|
+
|
|
228
|
+
root.appendChild(body);
|
|
229
|
+
|
|
230
|
+
if (headerSlot) {
|
|
231
|
+
try {
|
|
232
|
+
headerSlot.textContent = '';
|
|
233
|
+
headerSlot.appendChild(header);
|
|
234
|
+
} catch {
|
|
235
|
+
root.prepend(header);
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
root.prepend(header);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
container.textContent = '';
|
|
243
|
+
} catch {
|
|
244
|
+
// ignore
|
|
245
|
+
}
|
|
246
|
+
container.appendChild(root);
|
|
247
|
+
|
|
248
|
+
return () => {
|
|
249
|
+
if (chatUnsub) {
|
|
250
|
+
try {
|
|
251
|
+
chatUnsub();
|
|
252
|
+
} catch {
|
|
253
|
+
// ignore
|
|
254
|
+
}
|
|
255
|
+
chatUnsub = null;
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
container.textContent = '';
|
|
259
|
+
} catch {
|
|
260
|
+
// ignore
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Server 入口(可选)
|
|
3
|
+
*
|
|
4
|
+
* 注意:
|
|
5
|
+
* - ChatOS 导入插件包时会默认排除 `node_modules/`,因此这里不要依赖“随包携带的依赖”。
|
|
6
|
+
* - 若你需要使用 `@modelcontextprotocol/sdk`,请在 build 阶段做 bundle(把依赖打进单文件)。
|
|
7
|
+
*
|
|
8
|
+
* 你可以:
|
|
9
|
+
* 1) 用 bundler(esbuild/rollup)把 MCP Server 打包成单文件,并在 plugin.json 里把 `ai.mcp.entry` 指向打包产物;
|
|
10
|
+
* 2) 或者把依赖源码 vendoring 到插件目录内,使用相对路径 import。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// TODO: 实现你自己的 MCP Server(stdio)。建议把日志写到 stderr,不要污染 stdout。
|
|
14
|
+
console.error('[mcp] placeholder: implement your MCP server here');
|
|
15
|
+
process.exit(1);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export async function createUiAppsBackend(ctx) {
|
|
2
|
+
const llmComplete = async (params, runtimeCtx) => {
|
|
3
|
+
const api = runtimeCtx?.llm || ctx?.llm || null;
|
|
4
|
+
if (!api || typeof api.complete !== 'function') {
|
|
5
|
+
throw new Error('Host LLM bridge is not available (ctx.llm.complete)');
|
|
6
|
+
}
|
|
7
|
+
const input = typeof params?.input === 'string' ? params.input : typeof params?.prompt === 'string' ? params.prompt : '';
|
|
8
|
+
const normalized = String(input || '').trim();
|
|
9
|
+
if (!normalized) {
|
|
10
|
+
throw new Error('input is required');
|
|
11
|
+
}
|
|
12
|
+
return await api.complete({
|
|
13
|
+
input: normalized,
|
|
14
|
+
modelId: typeof params?.modelId === 'string' ? params.modelId : undefined,
|
|
15
|
+
modelName: typeof params?.modelName === 'string' ? params.modelName : undefined,
|
|
16
|
+
systemPrompt: typeof params?.systemPrompt === 'string' ? params.systemPrompt : undefined,
|
|
17
|
+
disableTools: params?.disableTools,
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
methods: {
|
|
23
|
+
async ping(params, runtimeCtx) {
|
|
24
|
+
return {
|
|
25
|
+
ok: true,
|
|
26
|
+
now: new Date().toISOString(),
|
|
27
|
+
pluginId: runtimeCtx?.pluginId || ctx?.pluginId || '',
|
|
28
|
+
params: params ?? null,
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
async llmComplete(params, runtimeCtx) {
|
|
33
|
+
return await llmComplete(params, runtimeCtx);
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# __PLUGIN_NAME__(Notepad 示例模板)
|
|
2
|
+
|
|
3
|
+
这是一个更接近“真实应用”的 **ChatOS UI Apps** 示例模板:Markdown 记事本(文件夹分类 + 标签检索 + 编辑/预览)。
|
|
4
|
+
|
|
5
|
+
## 快速开始
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npm run dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## 目录说明
|
|
13
|
+
|
|
14
|
+
- `plugin/plugin.json`:插件清单(应用列表、入口、后端、AI 贡献)
|
|
15
|
+
- `plugin/apps/__APP_ID__/`:前端 module(浏览器环境,导出 `mount({ container, host, slots })`)
|
|
16
|
+
- `plugin/backend/`:插件后端(Node/Electron main,导出 `createUiAppsBackend(ctx)`)
|
|
17
|
+
- `plugin/shared/`:共享存储实现(后端持久化所需)
|
|
18
|
+
- `docs/`:协议文档快照(随工程分发)
|
|
19
|
+
|
|
20
|
+
## 后端 API(示例)
|
|
21
|
+
|
|
22
|
+
前端通过 `host.backend.invoke(method, params)` 调用后端方法,本模板提供 `notes.*` 一组方法用于管理笔记:
|
|
23
|
+
|
|
24
|
+
- `notes.listFolders / notes.createFolder / notes.renameFolder / notes.deleteFolder`
|
|
25
|
+
- `notes.listNotes / notes.createNote / notes.getNote / notes.updateNote / notes.deleteNote`
|
|
26
|
+
- `notes.listTags / notes.searchNotes`
|
|
27
|
+
|
|
28
|
+
## MCP(可选)
|
|
29
|
+
|
|
30
|
+
模板内包含 `plugin/apps/__APP_ID__/mcp-server.mjs` 与 `mcp-prompt.*.md`,但默认 **未在** `plugin/plugin.json` 启用 `ai.mcp`(避免打包时遗漏依赖导致运行失败)。
|
|
31
|
+
|
|
32
|
+
如需启用 MCP:
|
|
33
|
+
|
|
34
|
+
1) 实现并 **bundle 成单文件**(建议 esbuild/rollup,把 `@modelcontextprotocol/sdk` 等依赖打进去)
|
|
35
|
+
2) 在 `plugin/plugin.json` 的 `apps[i].ai.mcp` 写入 `entry/command/args/...` 并指向 bundle 产物
|
|
36
|
+
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# ChatOS UI Apps:AI 暴露(MCP / Prompts)协议
|
|
2
|
+
|
|
3
|
+
本文件描述应用如何对 ChatOS 的 Chat Agent 暴露能力,包括:
|
|
4
|
+
|
|
5
|
+
- 声明一个专属 MCP Server(让工具随应用交付、随插件安装)
|
|
6
|
+
- 声明应用的默认 MCP Prompt(告诉模型如何使用这些工具/这个应用)
|
|
7
|
+
- 聚合暴露“已有的”全局 MCP Servers / Prompts(面板型应用常用)
|
|
8
|
+
- 内置应用的“开关 + 精细清单”两层机制(清单在 `aide` 维护)
|
|
9
|
+
|
|
10
|
+
实现对照(以代码为准):
|
|
11
|
+
|
|
12
|
+
- schema:`deepseek_cli/electron/ui-apps/schemas.js`
|
|
13
|
+
- 扫描与贡献解析:`deepseek_cli/electron/ui-apps/index.js`(`#resolveAi` / `getAiContribution()`;可选 `#syncAiContributes` 持久化到 Admin DB)
|
|
14
|
+
- 内置默认清单:`aide/shared/defaults/ui-apps-expose/`
|
|
15
|
+
|
|
16
|
+
## 1. 两种暴露方式(应用侧选择其一或组合)
|
|
17
|
+
|
|
18
|
+
### 方式 A:应用自带 MCP Server + MCP Prompt
|
|
19
|
+
|
|
20
|
+
用于:应用包含专属 tools(数据库查询、系统诊断、内部系统调用等)。
|
|
21
|
+
|
|
22
|
+
在 `apps[i].ai` 里声明:
|
|
23
|
+
|
|
24
|
+
- `ai.mcp`:宿主会创建/更新一个 MCP server(`serverName` 固定为 `${pluginId}.${appId}`)
|
|
25
|
+
- `ai.mcpPrompt`:宿主会创建/更新对应 system prompt(prompt 名称固定派生)
|
|
26
|
+
|
|
27
|
+
示例(节选):
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"id": "db-client",
|
|
32
|
+
"name": "数据库客户端",
|
|
33
|
+
"entry": { "type": "module", "path": "db-client/index.mjs" },
|
|
34
|
+
"ai": {
|
|
35
|
+
"mcp": { "entry": "db-client/mcp-server.mjs", "command": "node", "allowMain": true, "allowSub": true },
|
|
36
|
+
"mcpPrompt": { "zh": "db-client/mcp-prompt.zh.md", "en": "db-client/mcp-prompt.en.md" }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 方式 B:聚合暴露“已有的” MCP servers / Prompts(面板型应用常用)
|
|
42
|
+
|
|
43
|
+
用于:应用以“工作台/面板”形态聚合展示能力,不提供新的 MCP server,但需要按应用维度暴露已有 MCP servers / Prompts。
|
|
44
|
+
|
|
45
|
+
在 `apps[i].ai` 里声明:
|
|
46
|
+
|
|
47
|
+
- `ai.mcpServers: true | false | string[]`
|
|
48
|
+
- `ai.prompts: true | false | string[]`
|
|
49
|
+
|
|
50
|
+
注意:这是“可见/可选范围”的声明;最终是否启用、启用哪些,仍由 Agent 编辑页对该应用的多选结果决定。
|
|
51
|
+
|
|
52
|
+
## 2. 命名规则(宿主自动派生)
|
|
53
|
+
|
|
54
|
+
对每个 app:
|
|
55
|
+
|
|
56
|
+
- MCP `serverName`:`${pluginId}.${appId}`
|
|
57
|
+
- Prompt name:
|
|
58
|
+
- 中文:`mcp_<normalize(serverName)>`
|
|
59
|
+
- 英文:`mcp_<normalize(serverName)>__en`
|
|
60
|
+
|
|
61
|
+
`normalize()`:
|
|
62
|
+
|
|
63
|
+
- 转小写
|
|
64
|
+
- 把非 `[a-z0-9_-]` 的字符替换为 `_`
|
|
65
|
+
- 去掉首尾 `_`
|
|
66
|
+
|
|
67
|
+
## 3. `ai.mcp` 的 url 生成规则(entry → cmd://)
|
|
68
|
+
|
|
69
|
+
当 `ai.mcp.entry` 存在时,宿主会把它转换为:
|
|
70
|
+
|
|
71
|
+
- `cmd://<command> <absEntryOrRelEntry> <args...>`
|
|
72
|
+
- `command` 默认为 `node`
|
|
73
|
+
|
|
74
|
+
当 `ai.mcp.url` 存在时,直接使用该 url(远程/HTTP/WS 等)。
|
|
75
|
+
|
|
76
|
+
## 4. Agent UI 如何消费应用暴露(你需要知道的运行机制)
|
|
77
|
+
|
|
78
|
+
### 4.1 体现在哪里(持久化 vs 运行时注入)
|
|
79
|
+
|
|
80
|
+
当你把插件放进插件目录并在 UI「应用」页点击“刷新”后:
|
|
81
|
+
|
|
82
|
+
1) 宿主扫描插件并解析 `plugin.json`
|
|
83
|
+
2) 若发现 `apps[i].ai`:
|
|
84
|
+
- 宿主会在 UI Apps 注册表中暴露该应用的 `ai` 声明;
|
|
85
|
+
- **无论是否写入 Admin DB**,Chat Agent 运行时都可通过 `uiApps.getAiContribution()` 读取 `mcp.url` 与 `mcpPrompt` 文本,并按需“临时注入”到本次运行(见第 4.3 节)。
|
|
86
|
+
- (可选)若宿主启用 `syncAiContributes`,则会把 `ai.mcp` / `ai.mcpPrompt` **持久化同步**到 Admin DB(出现在 `MCP Servers` / `Prompts` 列表中,便于管理与复用)。
|
|
87
|
+
3) 在 `Chat → Agents` 创建/编辑 agent 时:
|
|
88
|
+
- 先选择应用(按应用维度)
|
|
89
|
+
- 再为每个应用分别勾选 MCP/Prompt,并在多选框中选择要挂载的 servers/prompts
|
|
90
|
+
|
|
91
|
+
### 4.2 保存结构(简化)
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"uiApps": [
|
|
96
|
+
{
|
|
97
|
+
"pluginId": "com.example.tools",
|
|
98
|
+
"appId": "db-client",
|
|
99
|
+
"mcp": true,
|
|
100
|
+
"prompt": true,
|
|
101
|
+
"mcpServerIds": ["<admin.mcpServers.id>", "..."],
|
|
102
|
+
"promptIds": ["<admin.prompts.id>", "..."]
|
|
103
|
+
}
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 4.3 运行时关键规则
|
|
109
|
+
|
|
110
|
+
- 主流程(MAIN)默认只启用 `allowMain=true` 的 MCP server(安全/权限控制)
|
|
111
|
+
- 如果希望 prompt 生效,请确保在应用下勾选 Prompt 并选择需要的 prompts(或提供 app 自己的 `ai.mcpPrompt`)
|
|
112
|
+
|
|
113
|
+
## 5. 内置应用的“开关 + 精细清单”机制
|
|
114
|
+
|
|
115
|
+
内置应用采用两层设计:
|
|
116
|
+
|
|
117
|
+
1) 宿主侧(`deepseek_cli` 的 `plugin.json`)只保留粗粒度开关:
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{ "ai": { "mcpServers": true, "prompts": true } }
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
2) 精细清单由应用团队在 `aide` 侧维护:
|
|
124
|
+
|
|
125
|
+
- 路径:`aide/shared/defaults/ui-apps-expose/<pluginId>__<appId>.yaml`(也支持 `.yml/.json`)
|
|
126
|
+
|
|
127
|
+
示例:
|
|
128
|
+
|
|
129
|
+
```yaml
|
|
130
|
+
mcpServers:
|
|
131
|
+
- project_files
|
|
132
|
+
- task_manager
|
|
133
|
+
|
|
134
|
+
prompts:
|
|
135
|
+
- default
|
|
136
|
+
- internal_main
|
|
137
|
+
- mcp_project_files
|
|
138
|
+
- mcp_task_manager
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
文件名规范:
|
|
142
|
+
|
|
143
|
+
- `<pluginId>__<appId>.yaml`(全部小写;不合法字符替换为 `_`)
|
|
144
|
+
|
|
145
|
+
重要:如果 `plugin.json` 未显式写 `ai.mcpServers/prompts`(或写成 `false`),宿主不会自动启用默认清单,避免“意外暴露”。
|
|
146
|
+
|
|
147
|
+
## 6. 暴露范围的合并优先级(精确规则)
|
|
148
|
+
|
|
149
|
+
对 `mcpServers` / `prompts` 分别适用相同规则(从高到低):
|
|
150
|
+
|
|
151
|
+
1) `plugin.json` inline 的 `ai.mcpServers/ai.prompts`
|
|
152
|
+
2) `ai.config` 文件(插件目录内)的 `mcpServers/prompts`
|
|
153
|
+
3) 内置默认清单(`aide/shared/defaults/ui-apps-expose`)
|
|
154
|
+
|
|
155
|
+
规则要点:
|
|
156
|
+
|
|
157
|
+
- inline 为 `false`:强制关闭(无视文件与默认清单)
|
|
158
|
+
- inline 为 `string[]`:强制使用该列表
|
|
159
|
+
- inline 为 `true`:优先用 `ai.config` 的值;若没有,再用默认清单;若仍没有,表示“全部”
|
|
160
|
+
- inline 未设置:只允许 `ai.config` 生效(不会自动启用默认清单)
|
|
161
|
+
|
|
162
|
+
## 7. MCP Server 最小骨架(Node.js / stdio)
|
|
163
|
+
|
|
164
|
+
MCP server 进程通过 stdio 与宿主通信(不要输出到 stdout,日志写 stderr):
|
|
165
|
+
|
|
166
|
+
```js
|
|
167
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
168
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
169
|
+
|
|
170
|
+
const server = new McpServer({ name: 'my_server', version: '0.1.0' });
|
|
171
|
+
|
|
172
|
+
// server.tool('tool_name', schema, handler) ...
|
|
173
|
+
|
|
174
|
+
const transport = new StdioServerTransport();
|
|
175
|
+
await server.connect(transport);
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
提示:
|
|
179
|
+
|
|
180
|
+
- tools 最终名字通常会变成 `mcp_<serverName>_<toolName>`(由宿主注册/派生)
|
|
181
|
+
- `serverName` 为 MCP server 的 `name`(应用方式 A 下由宿主固定派生为 `${pluginId}.${appId}`)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# ChatOS UI Apps:插件后端协议(Electron main 进程)
|
|
2
|
+
|
|
3
|
+
插件后端用于承载需要 Node 能力的逻辑(数据库/SSH/I/O/调用本地二进制等)。后端运行在 Electron main 进程,通过 IPC 由 `module` 前端调用。
|
|
4
|
+
|
|
5
|
+
实现对照(以代码为准):
|
|
6
|
+
|
|
7
|
+
- `deepseek_cli/electron/ui-apps/index.js`(`uiApps:invoke` 与后端加载/缓存)
|
|
8
|
+
|
|
9
|
+
## 1. 开启后端:`plugin.json`
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"backend": { "entry": "backend/index.mjs" }
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
约束:
|
|
18
|
+
|
|
19
|
+
- `backend.entry` 必须位于插件目录内;
|
|
20
|
+
- 必须是文件;
|
|
21
|
+
- 文件变更后会按 `mtime` 自动 reload(旧实例会尝试调用 `dispose()`)。
|
|
22
|
+
|
|
23
|
+
## 2. 后端入口:`createUiAppsBackend(ctx)`
|
|
24
|
+
|
|
25
|
+
后端入口模块必须导出:
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
export async function createUiAppsBackend(ctx) {
|
|
29
|
+
return {
|
|
30
|
+
methods: {
|
|
31
|
+
async ping(params, ctx2) {
|
|
32
|
+
return { ok: true, echo: params, pluginId: ctx2.pluginId };
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
async dispose() {},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
硬约束:
|
|
41
|
+
|
|
42
|
+
- 必须导出函数 `createUiAppsBackend`
|
|
43
|
+
- `createUiAppsBackend()` 返回值必须包含 `{ methods }`
|
|
44
|
+
|
|
45
|
+
方法调用约定:
|
|
46
|
+
|
|
47
|
+
- 前端调用:`await host.backend.invoke('ping', params)`
|
|
48
|
+
- 实际执行:`methods[method](params, ctx)`
|
|
49
|
+
- 第二个参数 `ctx` 会再次传入(即使你在 `createUiAppsBackend(ctx)` 的闭包里也已拿到一份)
|
|
50
|
+
|
|
51
|
+
## 3. `ctx`(宿主注入的运行时上下文)
|
|
52
|
+
|
|
53
|
+
`ctx` 字段(当前实现):
|
|
54
|
+
|
|
55
|
+
- `pluginId`:插件 ID
|
|
56
|
+
- `pluginDir`:插件安装目录(只读引用;用于读取插件资源)
|
|
57
|
+
- `dataDir`:`<stateDir>/ui_apps/data/<pluginId>`(插件可写数据目录;宿主会确保存在)
|
|
58
|
+
- `stateDir`:`<home>/.deepseek_cli/<hostApp>`(ChatOS 的 `hostApp=chatos`)
|
|
59
|
+
- `sessionRoot`:会话根
|
|
60
|
+
- `projectRoot`:宿主工程根(开发态下有用)
|
|
61
|
+
- `llm`:可选(共享模型调用接口)
|
|
62
|
+
- `ctx.llm.complete({ input, modelId?, modelName?, systemPrompt?, disableTools? })`
|
|
63
|
+
- 在 ChatOS 宿主实现中,`disableTools` 默认启用(除非显式传 `disableTools:false`);返回形如 `{ ok:true, model, content }`
|
|
64
|
+
|
|
65
|
+
规则:
|
|
66
|
+
|
|
67
|
+
- 持久化只写 `dataDir`,并自行做版本化与迁移。
|
|
68
|
+
- 返回值请保持可 JSON 序列化(避免传递函数/循环引用)。
|
|
69
|
+
- 插件后端不保存或读取 API Keys;密钥由宿主注入到环境变量;模型调用通过 `ctx.llm.complete()`。
|
|
70
|
+
|
|
71
|
+
## 4. 错误处理与返回结构
|
|
72
|
+
|
|
73
|
+
- 后端方法抛错会被宿主捕获并返回 `{ ok:false, message }`;
|
|
74
|
+
- 前端侧 `host.backend.invoke()` 会把 `ok:false` 转成异常抛出(便于应用用 `try/catch` 处理)。
|